Compare commits

..

453 Commits
v0.6.1 ... main

Author SHA1 Message Date
nadoo
bb439c9345 redir: fix test error
Some checks failed
Build / Build (push) Has been cancelled
2025-02-28 00:42:46 +08:00
nadoo
6aee7b35c0 chore: update math/rand to math/rand/v2 2025-02-28 00:40:20 +08:00
nadoo
8e81e09a8f pool: optimized the bytes buffer pool
Some checks are pending
Build / Build (push) Waiting to run
2025-02-26 21:44:54 +08:00
nadoo
bd40b07388 chore: update dependencies and optimize some code style 2025-02-26 21:34:43 +08:00
nadoo
40809b56a9 chore: use Go1.24
Some checks failed
Build / Build (push) Has been cancelled
2025-02-22 12:27:37 +08:00
nadoo
2f154678a9 docker: update binary folder 2024-12-21 23:36:52 +08:00
nadoo
b598c03dab chore: update dependencies 2024-12-21 20:05:13 +08:00
nadoo
1108ef29a0 chore: update dependencies 2024-08-16 22:12:40 +08:00
nadoo
708db591e9 ci: use Go1.23 2024-08-15 00:10:25 +08:00
nadoo
0d75bbda7e chore: update dependencies 2024-08-14 22:58:09 +08:00
nadoo
62f2a85677 chore: update dependencies 2024-08-06 22:03:02 +08:00
nadoo
7d4075282d dns: change UDPMaxLen to 1232 bytes 2024-08-05 23:31:19 +08:00
nadoo
c71c95482a chore: update dependencies 2024-04-28 23:03:41 +08:00
Lentin
4b8b05aa3e
chore: update glider.conf.example (#403)
fix https://github.com/nadoo/glider/issues/354
2024-04-28 22:51:32 +08:00
nadoo
6d2b1e95cc chore: change config example to default listen on 127.0.0.1 (fix #391) 2024-01-29 22:30:28 +08:00
nadoo
80a7d3b7fd dhcpd: use unicast to reply messages 2024-01-29 21:10:02 +08:00
nadoo
7016a3d340 dhcpd: handle client requested ip option 2024-01-26 18:53:46 +08:00
nadoo
c7d072372b chore: update dependencies 2023-11-28 18:26:33 +08:00
nadoo
d0e2d9be42 Merge branch 'master' into dev 2023-03-20 21:06:16 +08:00
Felix Yan
4f12a4f308
chore: Correct a typo in config/README.md (#365) 2023-03-20 21:03:10 +08:00
nadoo
6815f866cb goreleaser: remove deprecated options 2023-03-13 20:42:36 +08:00
nadoo
7e800555d7 chore(deps): update smux pkg 2023-03-09 18:36:30 +08:00
nadoo
8d0d8881b1 chore: use Go1.20 2023-03-07 18:39:49 +08:00
nadoo
03157367ca dhcpd: do not release static records 2022-12-19 00:01:14 +08:00
nadoo
d57d35c062 chore: fix typo and update dependencies 2022-06-02 09:59:51 +08:00
xfzka
0ef0208615
chore: Update README.md (#327)
if only wanna listen dns port, we need remove '-listen' option, otherwise can't run. will report error: `log.go:40: [mixed] failed to listen on -dns=:53: listen tcp: lookup -dns=: no such host`
2022-05-27 18:22:43 +08:00
nadoo
badb17e921 chore: do not override config file when installing deb package 2022-05-13 18:46:39 +08:00
nadoo
d1eacebd25 chore: bump version to v0.16.2 2022-05-12 23:25:25 +08:00
nadoo
1c0106ce6b fix(ssh): optimized the connection initialization 2022-05-08 23:57:55 +08:00
nadoo
1e01d8692d ssh: fixed an issue for unreachable addrs (fix #324) 2022-05-05 17:12:45 +08:00
nadoo
846ca0b699 chore: fixed typo and add some help info 2022-05-03 18:19:33 +08:00
nadoo
401efd621a chore: bump version to v0.16.1 2022-04-24 10:05:47 +08:00
nadoo
6c88d8bde4 socks5: avoid untyped nil printing (fix #322) 2022-04-24 09:59:12 +08:00
nadoo
decabcca6a ci: build docker image for riscv64 platform 2022-04-22 23:30:39 +08:00
nadoo
341a309198 ci: fix build error and build for architecture level x86-64-v3
ref:
https://github.com/golang/go/wiki/MinimumRequirements#amd64

https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels

https://en.wikipedia.org/wiki/Advanced_Vector_Extensions#Advanced_Vector_Extensions_2
2022-04-19 21:43:35 +08:00
nadoo
e862ba79f1 docker: add tzdata to set the timezone when running container
e.g:
-e "TZ=Europe/London"
2022-04-19 19:09:20 +08:00
nadoo
a153c34e37 vsock: update dependency to fix building error on riscv64 (#295) 2022-04-06 18:47:04 +08:00
nadoo
730e9c765e proxy: added vsock support (#295) 2022-04-05 15:22:12 +08:00
nadoo
ed7ee6bcd4 chore: update doc and change package path 2022-04-05 09:30:04 +08:00
nadoo
b0584be6cc chore: fix typo 2022-03-16 18:49:51 +08:00
nadoo
7c18fd7d42 ci: change to Go1.18 2022-03-16 12:01:09 +08:00
nadoo
3740375569 chore: doc updates 2022-03-14 13:06:26 +08:00
nadoo
420ec26368 proxy: move usage func to proxy.go 2022-03-13 18:03:10 +08:00
nadoo
a98995e2cb proxy: nat fullcone support for tproxy, trojan, ss, socks5 (fix #253) 2022-03-11 19:08:07 +08:00
nadoo
d68f361c35 ci: fix build error (#314) 2022-03-11 13:10:59 +08:00
Juan Calderon-Perez
1b972af52c
docker: Upgrade base alpine packages, run glider as non-root (#314) 2022-03-11 11:48:22 +08:00
nadoo
c9c2ce995f proxy: delete session when dial failed 2022-03-06 13:09:37 +08:00
nadoo
fc3a21617e chore: code tidy and doc update 2022-03-06 12:58:20 +08:00
nadoo
2ad3498abd proxy: optimize timeout handling in udp copy 2022-02-26 22:32:11 +08:00
nadoo
4b313a3fe1 dhcpd: fix build error 2022-02-24 18:44:52 +08:00
nadoo
9f6e5ebb98 dhcpd: fixed a bug in discovery 2022-02-24 18:41:03 +08:00
nadoo
c261e5989c dhcpd: added dhcpd-failover service, only keep linux support 2022-02-23 22:20:06 +08:00
nadoo
1f196c9cf5 dhcpd: check server existence on more platforms 2022-02-23 17:48:43 +08:00
nadoo
f96ad73c7d check: set checklatencysamples default to 10 (#282) 2022-02-20 21:37:36 +08:00
nadoo
fa97a44e8d check: added new config checklatencysamples to support average latency calculating (#282) 2022-02-20 19:50:23 +08:00
nadoo
cb698713ee dns: log error when adding an invalid record 2022-02-20 01:07:51 +08:00
nadoo
813c5fef94 config: added new flag example to print usage examples 2022-02-16 23:37:25 +08:00
nadoo
5cfb20562a doc: update documents 2022-02-16 00:25:14 +08:00
nadoo
cc63a59f1e proxy: support registerable usage info 2022-02-15 21:34:55 +08:00
nadoo
d05d71e591 ci: uload artifacts after building 2022-02-14 12:12:37 +08:00
nadoo
f358a1e877 config: support environment variable parsing (#311) 2022-02-08 15:39:18 +08:00
nadoo
7f925fb711 dns, ipset, rule: change dns AnswerHandler to use netip instead of string 2022-02-03 00:04:17 +08:00
nadoo
fdb32370e9 ci: combine build and docker process 2022-01-31 11:51:32 +08:00
nadoo
6cfbfff75f config: optimize the parsing speed of rule config file (#306) 2022-01-30 12:34:14 +08:00
nadoo
f2eb638b91 chore: optimize dhcpd and dns and more 2022-01-29 21:10:09 +08:00
nadoo
7e7c7553cc proxy: improve addr handling 2022-01-28 23:35:29 +08:00
nadoo
e12642b47a chore: small optimizations 2022-01-28 15:12:02 +08:00
nadoo
a814f8c545 chore: add cache for github actions 2022-01-27 17:15:04 +08:00
nadoo
9b2f00f4c8 proxy: add new env: FORWARDER_URL to script health check (#310) 2022-01-27 12:42:49 +08:00
nadoo
fac4b86f60 chore: use go1.18beta1 2022-01-26 23:40:49 +08:00
nadoo
755a8ca565 chore: document updates 2022-01-26 22:31:56 +08:00
nadoo
b0b043a280 tproxy: setsockopt in listener to suuport ipv6 2022-01-25 11:16:24 +08:00
nadoo
792b244c59 tproxy: try to fix ipv6 support (#290) 2022-01-24 22:56:59 +08:00
nadoo
d3fbef02bb dns: just return the request packet back when dnsnoaaaa enabled 2022-01-23 11:09:35 +08:00
nadoo
773d5d3b9d dns: fixed an issue 2022-01-23 00:42:54 +08:00
nadoo
b21ce3394d ipset: optimize codes 2022-01-23 00:13:49 +08:00
nadoo
faae2a9e22 ipset: support ipv6 2022-01-22 23:33:08 +08:00
nadoo
a919ac3469 dns: send nil response instead of dropping it when dnsnoaaaa is enabled 2022-01-22 13:13:20 +08:00
nadoo
2754fdeb60 dns: added new config dnsnoaaaa to filter AAAA response, default to false 2022-01-20 21:59:40 +08:00
nadoo
27de61a59d unix: use net.Dial to avoid interface binding Influence 2022-01-19 23:32:44 +08:00
nadoo
720f12aa0a socks5: support protocol chain with unix socket(#291) 2022-01-19 23:25:58 +08:00
nadoo
cf1a4e3817 proxy: added PackerServer interface 2022-01-16 23:15:18 +08:00
Koen Serry
253e5008c4
Added debian/ubuntu package (#307) 2022-01-13 18:46:21 +08:00
nadoo
0238b3fcce chore: build goarch riscv64 2022-01-10 20:09:26 +08:00
nadoo
a8d4e8d7ca chore: update documents 2022-01-08 23:57:30 +08:00
nadoo
1fd59a1677 chore: move some packages to pkg folder 2022-01-08 15:05:55 +08:00
nadoo
a529261893 vmess: support zero security 2022-01-01 00:44:31 +08:00
nadoo
39ae201afe proxy: improve interface binding (by setsockopt) on linux 2021-12-28 20:11:28 +08:00
nadoo
5b1a127d04 chore: optimize codes (golint) and github actions 2021-12-25 17:55:39 +08:00
nadoo
40f75ef38b vmess: fix aead painc #301 (ref: #302) optimize 2021-12-25 12:28:09 +08:00
nadoo
e3f7555032 vmess: fix aead painc #301 (ref: #302) 2021-12-24 19:40:36 +08:00
nadoo
55ab44fc90 proxy: support regex match in http(s) health checking 2021-12-23 00:17:28 +08:00
nadoo
71c7cd2823 proxy: support https health checking 2021-12-22 21:20:29 +08:00
nadoo
826695df9a chore(systemd): use DynamicUser 2021-12-20 22:41:44 +08:00
nadoo
b502b129b7 proxy: exit glider when it fails to listen (#284) 2021-12-11 12:19:40 +08:00
nadoo
3c431ee6e1 group: show the count of enabled proxies (#292) 2021-12-10 21:24:39 +08:00
nadoo
2c8a5065fa forward: allow duplicate forwarders 2021-12-10 18:32:41 +08:00
nadoo
5cbfcf815f ssh: added timeout parameter (optimize #289 @299) 2021-12-09 19:22:12 +08:00
nadoo
ff09c45fb6 ssh: init conn in Dial 2021-12-06 23:53:10 +08:00
nadoo
638adc63d6 ssh: try to fix #299 2021-12-05 19:03:57 +08:00
nadoo
346db3338e wss: optimize server code 2021-11-29 00:00:22 +08:00
nadoo
63caac133d Merge branch 'master' into dev 2021-11-28 23:17:10 +08:00
mzz
ca320d3c7f
fix(trojan): potential memory leaking (#297) 2021-11-28 23:14:27 +08:00
mzz
6006ec13c7
feat(vmess): add length mask (opt=4) (#298) 2021-11-28 23:02:46 +08:00
nadoo
807aebc678 tproxy: fix ipv6 support (#290) 2021-10-22 18:36:09 +08:00
nadoo
e5031ac8d5 smux: update to v1.5.16 2021-09-22 22:49:33 +08:00
nadoo
ce85f15c4b ci: use the latest go version 2021-09-14 00:56:10 +08:00
emptyteeth
83a43b7e0c
chore: fix wrong links in config/README.md (#280) 2021-09-10 23:49:35 -05:00
nadoo
6bd77a0b2a dns: support default port 53 in dnsserver config 2021-09-10 23:36:25 +08:00
nadoo
c86cadb4be tls,wss,trojan: support certificate pinning (#275) 2021-08-20 18:47:15 +08:00
nadoo
5578b19f61 ci: update action files to use Go1.17 2021-08-17 10:12:34 +08:00
nadoo
c055c33143 tls: added new param alpn 2021-08-17 09:39:24 +08:00
nadoo
8b29af3c0c dhcpd: support macOS and freebsd(not tested) 2021-08-12 19:11:06 +08:00
nadoo
7c50915f20 ws: remove the Sec-WebSocket-Protocol header 2021-08-08 23:30:29 +08:00
nadoo
aae2fc8256 vless,vmess: support string to uuid mapping standard 2021-08-08 00:05:47 +08:00
nadoo
926b364400 doc: update documents and help info 2021-08-05 23:52:31 +08:00
nadoo
8729908660 vmess: support aead auth 2021-08-05 23:32:53 +08:00
nadoo
7486373821 proxy: support server mode of PROXY protocol v1 2021-08-04 19:13:22 +08:00
nadoo
32e1c37cfe socks5,ss: improve udp server 2021-07-31 21:49:58 +08:00
nadoo
f3dc252967 udp,unix,tproxy: improve udp server 2021-07-29 20:05:20 +08:00
nadoo
a08c939dac trojan,vless: use proxy.RelayUDP to support ReadDeadline 2021-07-26 23:14:35 +08:00
nadoo
f66303b38d tproxy: fix data race 2021-07-26 13:05:19 +08:00
nadoo
b38f8a8761 tproxy: use goroutine to serve sessions 2021-07-26 00:42:21 +08:00
nadoo
d615dc087e config: added tcpbufsize & udpbufsize(default: 2048) 2021-07-24 23:45:53 +08:00
nadoo
41861ff48e tproxy: optimize codes and now works properly 2021-07-19 23:06:28 +08:00
nadoo
650f1eed4e tproxy: use nativeEndian instead of LittleEndian 2021-07-18 01:09:37 +08:00
nadoo
affa00a871 doc: add info for tproxy 2021-07-18 00:31:28 +08:00
nadoo
9af5ca9baf config: added config logflags 2021-07-17 23:43:22 +08:00
nadoo
56277acb7d proxy: added tproxy module (only udp now) 2021-07-17 22:36:42 +08:00
nadoo
15f9e74e39 wss: added a new scheme wss for convenience 2021-07-09 19:17:16 +08:00
nadoo
a62674838e ssh: handle error in init conn (#264) 2021-07-07 19:05:26 +08:00
nadoo
879e736f01 ws: use default port if not specified 2021-07-06 20:31:39 +08:00
nadoo
53fe94251f direct: optimize codes 2021-07-02 19:09:01 +08:00
nadoo
7f85f664e3 general: fixed a bug which leads to panic when the specified interface not exists 2021-07-02 01:06:11 +08:00
nadoo
72e57ab6e3 proxy: added a new scheme direct so we can use it as a failover forwarder 2021-06-29 19:55:05 +08:00
nadoo
d92e7f6191 dhcpd: support static ip address for mac 2021-06-10 20:09:21 +08:00
nadoo
41fee381d0 dhcpd: support to handle DECLINE & RELEASE msg 2021-06-07 19:14:58 +08:00
nadoo
3092d857ff dns: added new config dnscachelog and default to false 2021-05-30 12:18:05 +08:00
nadoo
ceb8b82df8 proxy: support chain more than 2 protocols in server mode 2021-05-05 18:41:48 +08:00
nadoo
70cf30b5e6 smux: return os.ErrDeadlineExceeded when timeout 2021-05-01 00:32:55 +08:00
nadoo
e9e4515e67 smux: improve smux server 2021-04-28 00:53:41 +08:00
Juan Calderon-Perez
074ca54053
Update Dockerfile (#258)
Fix for alpine cache error.
2021-04-22 12:17:21 +08:00
Juan Calderon-Perez
c5fd785cde
Syntax fix for Dockerfile (#257)
Added missing backslash on Dockerfile.
2021-04-22 10:32:34 +08:00
Juan Calderon-Perez
bcf17ade29
Add basic tools to Dockerfile (#255)
* Update Dockerfile

Add bind-tools and ca-certificates to Docker Image

* Support for non-root container

Added glider user and group to final image. Support for running Glider container as non-root user. Build image is now pinned to Golang 1.16 to avoid future issues when 1.17 is released.
2021-04-22 09:53:47 +08:00
nadoo
dbd2e04521 proxy: added smux support 2021-04-21 00:29:17 +08:00
nadoo
34a053b875 ssh: close unused connection (#251) 2021-04-20 20:55:40 +08:00
nadoo
ddfcaae49c ssh: reuse underlying connection (#251) 2021-04-20 15:28:52 +08:00
nadoo
2a7eed7667 ci: added freebsd version 2021-04-19 10:39:40 +08:00
nadoo
2d16870803 tls,trojan: optimize the default port handling 2021-03-21 00:08:00 +08:00
nadoo
93728c6378 socks5: fixed a bug in target getting 2021-03-19 15:57:16 +08:00
nadoo
88f09b30bc action: do not check issues with lable bug and enhancement. 2021-03-08 12:05:01 +08:00
nadoo
8116b5994b chore: bump version to v0.13.1 2021-02-19 23:23:12 +08:00
nadoo
4fab9d7b4b ci: use stable version of go1.16 2021-02-17 11:42:03 +08:00
nadoo
8bca9cb3e4 ws: avoid pannic when addr not set(#229) 2021-02-14 11:01:52 +08:00
nadoo
fbf694f5cd socks5: fix an issue in udp handling with auth (#219) 2021-02-12 22:11:07 +08:00
mzz
d2268b623f
fix(socks5): should not dial returned bind addr directly (#212)
* fix(socks5): should not dial returned bind addr directly
When server returns an any ip (0.0.0.0 or [::0]), we should use conventional ip to replace the any ip given (0.0.0.0 or [::0]).
This behaviour adapts to most situations.

See v2fly/v2ray-core#523

* fix: splithostport
2021-02-07 11:04:58 +08:00
nadoo
a310635a9f ci: remove tip version 2021-02-06 00:47:43 +08:00
nadoo
bf8e37c6df ci: fix issue in github action 2021-02-06 00:44:03 +08:00
nadoo
40aadf3e24 ci: use go1.16rc1 2021-02-06 00:26:58 +08:00
mzz
068281cafa
feat: support socks4a (#206)
* feat: support socks4a
* fix: the ip should be 'To4'
* fix: align length
2020-12-19 13:19:16 +08:00
nadoo
226dd97d7d ss: change the conn close behavior (fixed #180) 2020-12-15 19:26:25 +08:00
nadoo
42c15b9262 chore: document update 2020-12-04 18:42:16 +08:00
nadoo
03be29daba chore: add schemes section to doc 2020-12-04 12:49:49 +08:00
nadoo
e6631aaf84 chore: document update 2020-12-04 12:41:38 +08:00
nadoo
10b7f2d5e6 unix, udp: fixed nat issue (#194) 2020-12-03 23:47:26 +08:00
mzz
539f604e37
vmess(security-auto): choose security automaticly rather than using obsolete format (#200) 2020-12-03 17:12:16 +08:00
mzz
1b484cca8f
feat(vmess): support forward/udp (#199) 2020-12-03 15:27:52 +08:00
nadoo
2db84bb7aa chore: document works and code optimizations 2020-12-02 19:00:39 +08:00
nadoo
142865535e unix: support udp forwarding (#194) 2020-12-01 20:04:47 +08:00
nadoo
63112a1509 kcp: support set mode (#197) 2020-11-30 20:28:09 +08:00
nadoo
e077cb86b4 proxy: support tunnel mode inkcp tls ws 2020-11-29 23:13:19 +08:00
nadoo
87f1f44912 unix: support tunnel mode (#194) 2020-11-29 21:59:20 +08:00
nadoo
c9f3c20bc1 proxy: drop tcptun and udptun, use tcp and udp instead 2020-11-29 15:09:44 +08:00
mzz
adfb2fb9b4
fix: udp packet unpacking failure (#198) 2020-11-29 14:47:54 +08:00
nadoo
f60d248ac7 check: fixed a bug in checking 2020-11-26 19:43:06 +08:00
nadoo
98ce20b295 check: stop checking when protocol not supported 2020-11-26 19:21:27 +08:00
nadoo
6820644073 proxy: added new scheme udp as replacement of udptun 2020-11-24 19:30:08 +08:00
nadoo
1fcfeabdb1 check: change fwdr status after script check (#195) 2020-11-21 01:33:18 +08:00
nadoo
eaaa6d5aa9 check: added script checker (#195) 2020-11-21 01:20:40 +08:00
nadoo
65d606d29c check: support full url check and expect resp setting(#195) 2020-11-20 18:11:25 +08:00
nadoo
38f84a625d proxy: added new scheme tcp as replacement of tcptun 2020-11-17 23:58:42 +08:00
nadoo
1173f533ec check: support tcp check, use checktype & checkaddr instead of checkwebsite 2020-11-17 20:46:55 +08:00
nadoo
e6e5c3d4b6 doc: add ghcr info for docker 2020-11-12 23:57:11 +08:00
nadoo
8fbd69d4cc docker: add token for ghcr 2020-11-12 23:47:41 +08:00
nadoo
da1075116f docker: publish to github container registry 2020-11-12 23:39:54 +08:00
nadoo
678e9b9845 doc: add info for docker 2020-11-12 22:39:08 +08:00
nadoo
453fe925b3 docker: added scripts to publish docker image 2020-11-12 22:08:02 +08:00
nadoo
4887089a4b ws: allow to set Origin header and default to nil 2020-11-07 19:20:35 +08:00
nadoo
8a2e95b17e chore: add comments to export functions 2020-11-06 22:47:33 +08:00
nadoo
417ac556f7 pool: added bufio.Reader pool 2020-11-03 22:52:50 +08:00
nadoo
c15c55fe05 ssr: move 3rd party ssr pkg to internal 2020-10-29 22:47:57 +08:00
nadoo
7c92aca331 ssr: a little modification to use buffer pool 2020-10-27 22:32:04 +08:00
nadoo
b3940e4b77 ss: export the cipher package 2020-10-27 13:11:40 +08:00
nadoo
dad45afb17 ss: use internal pkg instead 2020-10-26 21:43:56 +08:00
nadoo
32990ebf77 proxy: optimize Copy for network connection 2020-10-24 18:55:47 +08:00
nadoo
e9f6f15290 general: try to reduce tcp fragment 2020-10-23 22:29:12 +08:00
nadoo
aefb9cd7ae proxy: enable splice in relay when using proxy.Conn 2020-10-22 19:42:42 +08:00
nadoo
904757052d conn: change TCPBufSize to 32K as the golang standard lib did 2020-10-21 20:00:43 +08:00
nadoo
cd78995cd4 ws: optimized the frame implementation 2020-10-20 20:28:35 +08:00
nadoo
beec9d205f ws: support websocket server mode 2020-10-19 20:45:57 +08:00
nadoo
13babd9305 vless: fixed a bug in fallback processing 2020-10-15 19:34:00 +08:00
nadoo
d510ea45b0 udp: remove uottun and optimize udp implementation 2020-10-15 00:19:05 +08:00
nadoo
78e03d7fbf check: add checktolerance config for lha mode 2020-10-12 19:07:54 +08:00
nadoo
a1ff92201c trojan: support fallback 2020-10-11 19:53:38 +08:00
nadoo
6fff126e4b proxy: optimize NewConn, avoid loop 2020-10-11 18:46:15 +08:00
nadoo
40ddd1be3a trojan: add new scheme trojanc (trojan cleartext) 2020-10-11 01:15:28 +08:00
nadoo
175ef16a5c trojan: support listen as trojan server 2020-10-10 19:04:33 +08:00
nadoo
6eda2b79c8 dns: update cache when an item expired 2020-10-09 22:02:19 +08:00
nadoo
6d64ee4c0e dns: change cache to lrucache 2020-10-08 18:48:23 +08:00
nadoo
8495f16a76 tls, trojan: use 443 as default port if not specified 2020-10-07 21:06:49 +08:00
nadoo
e7ebcbcde5 vless: disable fallback by default 2020-10-04 23:32:21 +08:00
nadoo
829a0d7f80 vless: support fallback to a http server 2020-10-04 18:26:44 +08:00
nadoo
422869b37a vless: support udp in server mode 2020-10-04 00:10:24 +08:00
nadoo
cd42337169 chore: update doc for info and restructure codes 2020-10-03 22:42:34 +08:00
nadoo
8b6739f12c vless: spport server mode 2020-10-03 20:51:27 +08:00
nadoo
1ed7fbff65 doc: updates for new features 2020-10-02 19:31:03 +08:00
nadoo
b323a62ce6 vless: added udp support 2020-10-02 19:09:12 +08:00
nadoo
bc68535dcd doc: add info for customize building 2020-10-02 00:03:49 +08:00
nadoo
edfca215c5 general: restructure package, move socks to proxy 2020-10-01 22:49:14 +08:00
nadoo
bd358e4ae6 proxy: move conn to proxy package 2020-10-01 22:38:34 +08:00
nadoo
47e20cf7f4 conn: add CopyN function 2020-10-01 21:33:59 +08:00
nadoo
c01c2d1839 vless: added vless support 2020-10-01 20:59:45 +08:00
nadoo
02220387e9 dpcpd: handle errors 2020-10-01 19:29:53 +08:00
nadoo
a956e5811f dhcpd: support ip pool larger than a class C 2020-09-29 18:59:57 +08:00
nadoo
de8c08c7b2 dhcpd: assign random ip 2020-09-29 00:38:35 +08:00
nadoo
5b774cf90e dhcpd: added dhcp service (ipv4 only) 2020-09-28 00:49:58 +08:00
nadoo
4e966cc319 service: add service module 2020-09-27 19:50:21 +08:00
nadoo
9ca06ee32f general: add features.go 2020-09-27 14:51:36 +08:00
nadoo
3d61be2819 doc: fix typo 2020-09-27 00:08:16 +08:00
nadoo
72a2782395 config: better naming and some minor optimizations 2020-09-26 23:34:26 +08:00
nadoo
2d71facf9b ci: add workflow to publish docker image 2020-09-25 13:17:16 +08:00
Neeraj Mittal
e72086120a
docker: dockerfile added (#185) 2020-09-25 13:01:09 +08:00
nadoo
93a7677a94 ipset: use github.com/nadoo/ipset package 2020-09-25 11:04:13 +08:00
nadoo
04c65fb444 rule: optimized codes 2020-09-24 18:50:04 +08:00
nadoo
84b00d6db6 general: use builtin dns when it enabled. (#184) 2020-09-23 22:14:18 +08:00
nadoo
89114e678b proxy: record failures of remote connection only 2020-09-14 20:56:37 +08:00
nadoo
b3b11b4188 http: better logging 2020-09-13 23:28:01 +08:00
nadoo
0e89d1879f general: use builtin dns as resolver when dns enabled 2020-09-13 15:25:57 +08:00
nadoo
851fc46104 conn: optimize relay operation 2020-09-03 00:12:00 +08:00
nadoo
31f7c50cbc general: optimize memory allocations 2020-08-26 19:21:35 +08:00
nadoo
167e6e5d29 general: several optimizations 2020-08-25 22:14:08 +08:00
nadoo
f65a983da8 dns: optimize codes 2020-08-23 23:23:30 +08:00
nadoo
a42d3a68d0 dns: check length in UnmarshalDomain to avoid panic 2020-08-21 23:54:18 +08:00
nadoo
a118ec5837 general: optimize domain parts operations 2020-08-16 12:00:46 +08:00
nadoo
3a6fd63bc0 ssr: update version to fix checksum issue(#176) 2020-08-14 12:56:59 +08:00
nadoo
8981c759b4 general: remove unused lib 2020-08-13 21:21:19 +08:00
nadoo
997bf09c32 ci: update golang and action script version 2020-08-13 21:09:54 +08:00
nadoo
8f661a67a4 general: optimize codes 2020-08-10 18:43:41 +08:00
nadoo
3c509f8b7a pool: use bitwise operate instead of binary search 2020-08-07 22:41:28 +08:00
nadoo
2fee24995a pool: use binary search to find proper index 2020-08-07 12:07:30 +08:00
nadoo
382f9cc519 pool: fixed a bug in PutBuffer 2020-08-06 18:07:20 +08:00
nadoo
16eee62947 doc: add info to build from source code 2020-08-02 18:40:10 +08:00
nadoo
c43bf0bbbb ssr: update due to 3rd party package change 2020-07-23 21:12:48 +08:00
nadoo
e23a29f057 tls: support customization of serverName (#169) 2020-07-14 18:35:48 +08:00
nadoo
92a43608f3 ssr: update due to upstream lib change 2020-07-13 10:30:40 +08:00
nadoo
ab6a448d00 dns: optimize code 2020-05-06 20:10:18 +08:00
nadoo
e3c57ba369 dns: add length check to avoid panic 2020-05-05 01:30:57 +08:00
nadoo
665d722d2c direct: support dial timeout and relay timeout (#97) 2020-05-04 16:51:41 +08:00
nadoo
3392db41de check: stop current check process when timeout (#159) 2020-05-04 15:33:26 +08:00
nadoo
061b3da42e ssh: support ssh forwarder 2020-05-04 13:53:59 +08:00
nadoo
48e059db6c dns: switch upstream when dial error occurred 2020-05-03 20:02:11 +08:00
nadoo
2fe9c3990b forwarder: avoid to disable direct forwarder 2020-05-02 21:49:30 +08:00
nadoo
a9a1985a4b dns: allow to switch dns server permanently (#97) 2020-05-02 20:02:19 +08:00
nadoo
b99a730968 general: suport results feedback in more protocols 2020-05-01 20:54:23 +08:00
nadoo
c5881a6db3 general: update due to the previous interface change 2020-04-29 18:45:58 +08:00
wuudjac
22e0576b53
redir, dns: IncFailures on non-timeout errors (#133) 2020-04-28 15:18:19 +08:00
nadoo
c2d9c67214 dns: log qname in error 2020-04-28 12:23:21 +08:00
nadoo
76fddcc29c dns: use pool buffer if possible 2020-04-22 19:37:10 +08:00
nadoo
45f4240f73 rule: support checkdisabledonly option 2020-04-21 21:51:27 +08:00
nadoo
b74880ae7a pool: added a function to init bufPools 2020-04-21 12:17:14 +08:00
nadoo
0b0611a0dc strategy: added new option checkfailedonly #138 2020-04-21 00:50:12 +08:00
nadoo
149824cc20 dns: marshal to io.writer instead of byte slice 2020-04-20 12:41:53 +08:00
nadoo
a0542a028e pool: return specified size of buffer 2020-04-19 23:20:15 +08:00
nadoo
5326c0a901 pool: added buffer pool for conn 2020-04-19 17:03:39 +08:00
nadoo
8ccafa8fbd doc: update doc 2020-04-14 00:17:46 +08:00
nadoo
d8b9e00025 socks4: add doc info and fix bug 2020-04-13 12:33:44 +08:00
nadoo
fa3a7da859 socks4: fix compile problem 2020-04-13 12:26:21 +08:00
mr.The
f14c4c7797
socks4: add support for socks4 proxy (#114) 2020-04-13 12:22:03 +08:00
nadoo
8b43213c76 dns: code optimize 2020-04-13 00:55:11 +08:00
nadoo
ff461f615b doc: add some info for trojan 2020-04-12 17:48:04 +08:00
nadoo
792a47f0c0 doc: update doc for trojan 2020-04-12 17:24:34 +08:00
nadoo
bfcf9272dc trojan: added udp support 2020-04-12 15:27:20 +08:00
nadoo
2520a1c8b4 ss: handle unexpect situation to avoid panic #132 2020-04-11 21:08:10 +08:00
nadoo
4e1cb6ebf6 trojan: added trojan support 2020-04-08 01:09:51 +08:00
nadoo
d668339e27 ipset: add missing package 2020-04-06 23:37:36 +08:00
nadoo
a4346d2b03 general: minor changes in github actions and codes 2020-04-06 23:33:23 +08:00
nadoo
855907c46b ws: support set a different host header #157 2020-04-05 11:55:48 +08:00
nadoo
666c76f921 general: tidy code 2020-04-04 22:57:29 +08:00
nadoo
db9aa1bf6c goreleaser: update to support mips 2020-04-04 18:27:55 +08:00
nadoo
9c3f2f600a socks5: fix #148 2020-04-04 18:00:31 +08:00
nadoo
fda05d5d3d strategy: try fwrdrs 1 by 1 when len(avail)==0 2020-04-04 00:03:47 +08:00
nadoo
c4cebdd1d8 actions: update scripts 2020-04-03 22:54:13 +08:00
nadoo
fd3bdb8f56 strategy: run init when no forwarders available 2020-04-03 20:31:59 +08:00
nadoo
1204cf30fd strategy: try to fix #135 #142 2020-04-03 20:15:22 +08:00
nadoo
1789d97f00 ssr: change 3rd party module #139 2020-04-03 19:57:31 +08:00
nadoo
fc7060080c Merge branch 'master' into dev 2020-04-03 12:45:48 +08:00
nadoo
bd0d14159c http: fix #155 2020-04-03 12:42:57 +08:00
nadoo
4170da0f5d general: show help message in stdout 2019-10-21 21:03:19 +08:00
nadoo
ef2d3cd07f http: remove debug info 2019-10-21 15:51:38 +08:00
nadoo
393a334705 http: fix #127 2019-10-21 15:44:50 +08:00
nadoo
ca88797df4 http: fixed #128 2019-10-21 14:58:18 +08:00
nadoo
39cd60c50b http: close remote conn after relay 2019-10-21 14:16:49 +08:00
nadoo
486c455236 log: show caller's info in verbose mode 2019-10-20 17:52:25 +08:00
nadoo
ef8cdfa703 http: support auth in server mode. #84 2019-10-19 22:16:15 +08:00
nadoo
2560f6727f socks5: support auth in server mode. #84 2019-10-19 20:29:52 +08:00
nadoo
822693b05b mod: use new modules 2019-10-14 11:39:16 +08:00
nadoo
d9189e6212 http: add Host header in CONNECT request 2019-10-13 21:42:16 +08:00
nadoo
0c719d7098 ssr: fix #115 2019-10-11 19:05:05 +08:00
Felix Yan
6131a2de65 Add workaround for chacha20's repository move (#124)
With this fix applied #115 should go away. Currently used in Arch packaging.
2019-10-07 12:55:27 +08:00
nadoo
6604bfe8a7 tls: fixed a bug in tls which may lose proxy addr 2019-09-20 13:58:53 +08:00
nadoo
4cb9ea2bdc ss/socks5: fixed a bug in udp mode 2019-09-19 22:42:06 +08:00
nadoo
2c4aa26055 general: bump version 2019-09-19 18:03:48 +08:00
nadoo
d6d706b5ee log: show proxy info in log, via PROXY 2019-09-18 22:08:48 +08:00
nadoo
edcb21236a proxy: add a new interface proxy to distinguish client and server 2019-09-18 19:40:14 +08:00
nadoo
6c8141e43b strategy: return fwdr's addr when only 1 fwdr left 2019-09-18 17:10:33 +08:00
nadoo
063dc1bc01 proxy: added the ability to log forwarder 2019-09-18 12:53:04 +08:00
nadoo
c2425e67de general: add github actions and remove travis 2019-09-15 20:50:52 +08:00
nadoo
d36b8fa394 general: optimize code 2019-09-07 20:25:00 +08:00
LJ
69bbaa809c rule: log next dialer in verbose mode. (#92)
Print out the next dialer is useful when a particular website does not load completely, and it helps when updating config rules.
2019-09-07 17:05:19 +08:00
whoami
9595fcf0c0 typo: (#99) 2019-09-07 16:26:55 +08:00
whoami
63a38e70f0 typo: (#100) 2019-09-07 16:26:05 +08:00
nadoo
bb105589b6 doc: add config include example. #94 2019-03-21 21:15:31 +08:00
nadoo
8fa411cf52 doc: add example for reject. #94 2019-03-21 21:10:00 +08:00
nadoo
0ef3d72e2e mod: update go modules 2019-03-19 20:05:29 +08:00
nadoo
a22b1d9c86 strategy: add checktimeout config. (#89) 2019-03-18 23:37:01 +08:00
nadoo
47406ce4ce udptun: fixed a bug in nat mapping. #91 2019-03-12 23:32:23 +08:00
nadoo
4ece9ece25 dns: allow to resolve domain which in a reject rule 2019-03-08 00:14:37 +08:00
nadoo
2e44d79db8 reject: add a virtual proxy reject 2019-03-07 23:34:29 +08:00
nadoo
f9a21f5951 rule: allow to use root domains like com/net/cn... #43 2019-03-07 00:19:30 +08:00
nadoo
8c922db612 compiler: use go 1.12 2019-03-05 22:30:22 +08:00
nadoo
19acda9ee0 redir: avoid local loop request 2019-03-04 19:19:00 +08:00
nadoo
9bcc18a29b ci: update to go 1.12 2019-03-01 00:59:55 +08:00
nadoo
47547bb08d config: change checkduration to checkinterval 2019-01-06 21:01:20 +08:00
nadoo
c89d6018cf dns: set ttl to minttl config for custom dns recordings 2019-01-06 20:38:15 +08:00
nadoo
3c50c46eb9 general: use go mod instead of govendor 2018-12-20 00:13:57 +08:00
nadoo
d5a9d680a9 forwarder: set to disabled by default 2018-12-19 23:33:58 +08:00
nadoo
6ba9e1f5e8 obfs: compatible with some server implementation in tls mode 2018-12-16 13:28:24 +08:00
nadoo
51fedc8653 http: fix potential problem 2018-12-14 00:02:25 +08:00
nadoo
c6a879dded kcp: add some default settings to kcp conn (to improve the speed) 2018-12-13 00:22:34 +08:00
nadoo
750862abdb obfs: added simple-obfs support. #79 2018-12-12 21:40:31 +08:00
nadoo
da055c9078
Merge pull request #80 from kiitehq/fix/adds-path-http-method
adds HTTP PATCH support to the mixed proxy
2018-12-01 10:04:12 +08:00
Kurt Schwarz
54a75d6972 adds HTTP PATCH support to the mixed proxy 2018-11-30 15:19:55 -05:00
nadoo
b44fd344f4 kcp: added kcp support #63 2018-11-30 19:01:25 +08:00
nadoo
44c30df001 doc: update description for tls 2018-11-28 23:28:32 +08:00
nadoo
5a43cf873e ipset: only allow to set ipset in rule files #69 2018-11-27 23:25:20 +08:00
nadoo
86478d2c25 doc: update document for tls and unix proxy 2018-11-25 22:21:23 +08:00
nadoo
dadaf35cb2 unix: add unix domain socket support. #59 2018-11-25 17:56:59 +08:00
nadoo
25c9721b00 tls: optimized code 2018-11-25 15:41:47 +08:00
nadoo
d37c2e2a35 server: changed interface definition and implementation 2018-11-25 13:18:15 +08:00
nadoo
2813e80a98 http: fix #76 2018-11-21 20:28:46 +08:00
nadoo
266578d67d
Merge pull request #71 from changx/master
add tls:// transport layer for listeners
2018-10-29 23:02:07 +08:00
changx
88e33cab7e downgrade to tls12 2018-10-29 16:26:37 +08:00
changx
e27601f648 listener with tls transport layer 2018-10-29 16:18:51 +08:00
nadoo
14b072832d dns: SetDeadline for all remote connection 2018-10-20 21:24:11 +08:00
changx
5383ac4fc0 tls listener 2018-10-14 13:56:04 +08:00
nadoo
515a5dffa5 redir: fixed a typo 2018-09-17 19:22:36 +08:00
nadoo
15b98359b1 http: fixed a compatibility issue with some http proxy server. #62 2018-09-17 19:16:17 +08:00
nadoo
70a88f4789 redir: added redir6 proxy 2018-09-04 20:26:40 +08:00
nadoo
4e5ee78df9 ss, socks5: print udp log only when a nat map is created 2018-09-02 22:30:41 +08:00
nadoo
eae5bf118d general: add vendor folder 2018-09-02 22:16:24 +08:00
nadoo
3c8a451d94 ss: added chacha20 and xchacha20-ietf-poly1305 support. #21 #30 2018-09-02 19:18:51 +08:00
nadoo
7367e41cd1 dns: correct a length mistake 2018-09-02 00:16:16 +08:00
nadoo
1b4489cd67 dns: return error when there's not enough length for rdata. #55 2018-09-01 23:44:47 +08:00
nadoo
981d226bd1 strategy: display forwarder's addr instead of struct value. 2018-08-31 00:16:26 +08:00
nadoo
140ed9d8eb conf: support relative path of rulefile in systemd service. #52 2018-08-30 07:36:46 +08:00
nadoo
0170fd7c98 direct: avoid panic when an interface link is down 2018-08-30 07:35:37 +08:00
nadoo
1c2ad64000 strategy: when no forwarders, available set priority to 0 2018-08-29 23:54:00 +08:00
nadoo
ac94540ab8 general: update some doc or comment for new features. 2018-08-27 19:38:42 +08:00
nadoo
20616dfd10 doc: add info for dnsalwaystcp 2018-08-27 00:01:09 +08:00
nadoo
529786acf6 dns: add dnsalwaystcp config. #49 2018-08-26 22:36:14 +08:00
nadoo
2f239d60f1 doc: add info for dh mode 2018-08-26 01:43:28 +08:00
nadoo
035e15df5c strategy: optimized code 2018-08-26 01:25:22 +08:00
nadoo
bec2c1fd63 strategy: add the ability to handle forwarder status change events 2018-08-25 23:56:18 +08:00
nadoo
eb0b17bea1 strategy: add experimental dh: destination hashing mode 2018-08-24 18:45:57 +08:00
nadoo
104e51ae3c strategy: removed unnecessary funcs 2018-08-24 01:15:56 +08:00
nadoo
4f69372656 ha: change dialer when there's a forwarder enabled with higher priority 2018-08-24 00:57:55 +08:00
nadoo
b9017ff70a strategy: avoid data race 2018-08-23 00:01:31 +08:00
nadoo
0d1c4c5a47 direct: remove link-local ip from IFaceIPs func 2018-08-22 00:11:31 +08:00
nadoo
1d5ef167ec doc: add info for setting interface 2018-08-21 07:57:21 +08:00
nadoo
8daa7784b0 general: add global setting interface 2018-08-20 22:23:00 +08:00
nadoo
edd5964a1e direct: optimized local ip logic 2018-08-20 00:17:16 +08:00
nadoo
c9e9ebd287 forwarder: support curl like interface parameter 2018-08-19 01:49:52 +08:00
nadoo
c21a7d8c4d forawrder: change localip to interface 2018-08-18 23:59:21 +08:00
nadoo
59e7b17200 doc: add local ip info for forwarder 2018-08-17 23:53:27 +08:00
nadoo
88ff8997b5 strategy: avoid returning nil dialer in lha.nextDialer 2018-08-16 00:01:59 +08:00
nadoo
39ccbc5adf forwarder: add the ability to specify local ip. #48 2018-08-15 00:54:17 +08:00
nadoo
205b4efac7 dns: use Direct dialer when dialer's domain equals to the query domain 2018-08-14 22:32:55 +08:00
nadoo
fbb78d50d4 strategy: keep default priority as 0 to check all fwders at start time 2018-08-14 20:01:43 +08:00
nadoo
4967f0bd36 doc: add info for forwarder option: priority 2018-08-14 19:52:32 +08:00
nadoo
e82ea75cba strategy: experimental latency based high availability mode supported 2018-08-14 19:33:18 +08:00
nadoo
f4eab4d1b2 ipset: move to separate package 2018-08-13 00:42:59 +08:00
nadoo
978029bd2b rule: move to separate package 2018-08-12 22:24:49 +08:00
nadoo
3a8af82f87 conf: use dns.Config instead of multiple variables 2018-08-12 22:07:19 +08:00
nadoo
e0c07146dc strategy: do not check forwarders whose priority less than current value 2018-08-12 22:00:12 +08:00
nadoo
b5b7f2998b forwarder: set status to disabled when dialing fails MaxFailures times 2018-08-12 21:40:22 +08:00
nadoo
e1c318990b strategy: support priority now (need to check) 2018-08-12 18:50:44 +08:00
nadoo
a46ab20901 forwarder: add the ability to get parameters like 'priority' 2018-08-12 12:37:25 +08:00
nadoo
e3888a6bd3 strategy: moved to a separate package 2018-08-11 11:46:10 +08:00
nadoo
0da05ecedd general: add a forwarder struct and changed proxies to use it 2018-08-10 19:03:30 +08:00
nadoo
2fcef7b00c ws: fixed a bug when ws used as a middle layer protocol(tls,ws,vmess) 2018-08-08 00:03:32 +08:00
nadoo
a26e437b5d dns: add settings dnstimeout/dnsmaxttl/dnsminttl 2018-08-07 19:43:52 +08:00
nadoo
6744f1ad25 dns: correct setdeadline 2018-08-06 08:48:13 +08:00
nadoo
7274dcf622 dns: set timout only when multiple upstream dns servers set 2018-08-06 08:41:54 +08:00
nadoo
9c73e969ca dns: return correct err in exchange 2018-08-06 08:13:23 +08:00
nadoo
b27f153426 dns: check bytes length in unmarshal message 2018-08-06 08:03:07 +08:00
nadoo
5467e4bbc1 dns: add some comments 2018-08-06 00:46:07 +08:00
nadoo
09b6d59394 dns: support multiple upstream servers 2018-08-05 23:41:34 +08:00
nadoo
2cba536ab4 ipset: do not insert to ipset when forwarder is Direct 2018-08-04 16:39:53 +08:00
nadoo
1f8a3cdf53 general: remove unnecessary SetKeepAlive 2018-08-03 19:27:47 +08:00
nadoo
6cdfbc3552 dns: change non-expired ttl to 50years to avoid int overflow on 32bit os 2018-08-02 13:28:01 +08:00
nadoo
c049b75b64 systemd: add introduction for systemd file open limit 2018-08-02 13:16:05 +08:00
nadoo
73a778f5e1 common: remove unnecessary logs 2018-08-02 13:02:04 +08:00
nadoo
9acaff5b4a dns: query in udp when client requests in udp and no forwarder specified 2018-08-02 00:11:22 +08:00
nadoo
8d20331096 doc: update README file format 2018-08-01 00:47:43 +08:00
nadoo
04c6f74343 doc: update for custom dnsrecord 2018-08-01 00:36:11 +08:00
nadoo
a226637bfb dns: 1. support cache; 2. support custom records; #35 2018-08-01 00:09:55 +08:00
nadoo
a2a67df771 http: keep proxy-connection when connect to upstream http proxy. #42 2018-07-31 20:25:39 +08:00
nadoo
4781e7b472 dns: add tcp server support 2018-07-31 00:03:36 +08:00
nadoo
41ddbb1168 dns: fixed a bug in compressed domain offset calculation 2018-07-30 10:31:02 +08:00
nadoo
40f3315007 dnstun: remove dustun proxy, use dns and dnsserver settings instead 2018-07-30 01:13:44 +08:00
nadoo
f6a578f849 dns: changed UnmarshalMessage to return *Messaeg 2018-07-30 01:05:08 +08:00
nadoo
5e32133eb9 dns: fixed a bug in UnmarshalRR 2018-07-30 00:18:10 +08:00
nadoo
d5e3ea539a dns: rewrite codes 2018-07-29 23:44:23 +08:00
nadoo
7dc21908d5
Merge pull request #40 from soffchen/master
remove 'cidr=' duplicated
2018-07-27 20:34:13 +08:00
Soff
1acbf1448d Update README.md 2018-07-26 22:47:27 +08:00
Soff
490b992c91
Update office.list 2018-07-26 22:46:48 +08:00
nadoo
b2a4039c1d
Merge pull request #39 from soffchen/patch-4
Absolute path support for rules-dir config
2018-07-26 22:17:03 +08:00
Soff
3939c01467
Absolute path support for rules-dir config 2018-07-26 14:01:34 +08:00
nadoo
21923af1cd doc: update some descriptions 2018-07-25 08:23:58 +08:00
nadoo
49021f167c doc: update help msg for ws 2018-07-24 00:54:38 +08:00
nadoo
88eee75aa7 ws: add support for security key check 2018-07-24 00:45:41 +08:00
nadoo
68d8fb8b60 ws: fixed a bug in frame reader 2018-07-23 01:41:18 +08:00
nadoo
cdabe41e34
Merge pull request #37 from soffchen/patch-3
doc: Update README.md
2018-07-22 22:18:37 +08:00
nadoo
f9b16d9417
Merge pull request #36 from soffchen/patch-2
doc: Update glider.conf
2018-07-22 22:16:21 +08:00
Soff
b57db4f429
Update README.md
Missing colon
2018-07-22 22:12:17 +08:00
Soff
18114f1c33
Update glider.conf
Missing colon
2018-07-22 22:11:40 +08:00
nadoo
068e4df8fc doc: add some info for websocket 2018-07-22 20:02:50 +08:00
nadoo
bb45b671c0 general: add dev_linux conditional build codes 2018-07-22 18:54:18 +08:00
nadoo
ff1fb8c291 ws: fixed bug in ws codes and now it worked 2018-07-22 18:21:27 +08:00
nadoo
9856f943ad ws: add experimental websocket codes 2018-07-21 22:56:37 +08:00
nadoo
9d42885922 pprof: add pprof conditional compilation 2018-07-17 22:14:12 +08:00
nadoo
c8c4d5c75c doc: drop support for go1.9 2018-07-16 15:08:43 +08:00
nadoo
e0f746fa3d vmess: set rand.Seed only 1 time in NewClient 2018-07-14 12:18:52 +08:00
nadoo
7a7b25f173 doc: update for vmess security settings 2018-07-12 11:22:21 +08:00
nadoo
49482bf054 vmess: add rand.Seed to avoid same sequence values of rand.Read 2018-07-12 10:58:48 +08:00
nadoo
3c3cb8e3fd doc: update for vmess and tls 2018-07-11 08:34:15 +08:00
nadoo
ed18e8602d vmess: chacha20-poly1305 supported 2018-07-11 08:23:26 +08:00
nadoo
5ebbf7ffe8 vmess: fixed bug in aes-128-gcm 2018-07-11 00:50:38 +08:00
nadoo
d84f540c4d vmess: add aead chpher 2018-07-11 00:26:05 +08:00
nadoo
dc46705f3f vmess: fixed a bug in chunked writer 2018-07-10 09:44:51 +08:00
nadoo
fd298207c5 vmess: update chunk codes 2018-07-10 00:53:15 +08:00
nadoo
b465dc1444 vmess: support chunk stream 2018-07-09 23:42:33 +08:00
nadoo
e866062a8c rule: change log fmt 2018-07-08 23:22:01 +08:00
261 changed files with 15437 additions and 20368 deletions

38
.Dockerfile Normal file
View File

@ -0,0 +1,38 @@
# Build Stage
FROM --platform=$BUILDPLATFORM alpine AS build-env
COPY ./dist /dist
RUN apk add --no-cache ca-certificates
ARG TARGETPLATFORM
RUN case $TARGETPLATFORM in \
'linux/386') \
export FOLDER='default_linux_386_sse2'; \
;; \
'linux/amd64') \
export FOLDER='default_linux_amd64_v1'; \
;; \
'linux/arm/v6') \
export FOLDER='default_linux_arm_6'; \
;; \
'linux/arm/v7') \
export FOLDER='default_linux_arm_7'; \
;; \
'linux/arm64') \
export FOLDER='default_linux_arm64_v8.0'; \
;; \
'linux/riscv64') \
export FOLDER='default_linux_riscv64_rva20u64'; \
;; \
*) echo >&2 "error: unsupported architecture '$TARGETPLATFORM'"; exit 1 ;; \
esac \
&& mv /dist/$FOLDER /app
# Final Stage
FROM scratch
COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build-env /app /app
WORKDIR /app
USER 1000
ENTRYPOINT ["./glider"]

103
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,103 @@
name: Build
on:
push:
branches:
- "dev"
tags:
- "*"
pull_request:
env:
APP_NAME: glider
DOCKERHUB_REPO: nadoo/glider
GHCR_REPO: ghcr.io/nadoo/glider
PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/riscv64
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Vars
run: |
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v5
with:
check-latest: true
go-version-file: "go.mod"
cache: true
- name: Test
run: go test -v ./...
- name: Build
uses: goreleaser/goreleaser-action@v6
if: "!startsWith(github.ref, 'refs/tags/')"
with:
args: build --snapshot --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Artifacts
uses: actions/upload-artifact@v4
if: "!startsWith(github.ref, 'refs/tags/')"
with:
name: ${{ env.APP_NAME }}-dev-${{ env.SHA_SHORT }}
path: |
./dist/default_linux_amd64_v1/${{ env.APP_NAME }}
./dist/default_linux_arm64/${{ env.APP_NAME }}
./dist/default_darwin_arm64/${{ env.APP_NAME }}
./dist/default_windows_amd64_v1/${{ env.APP_NAME }}.exe
- name: Release
uses: goreleaser/goreleaser-action@v6
if: startsWith(github.ref, 'refs/tags/')
with:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Docker - Set up Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Docker - Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker - Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker - Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_REPO }}
${{ env.GHCR_REPO }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Docker - Build and push
uses: docker/build-push-action@v6
with:
context: .
file: .Dockerfile
platforms: ${{ env.PLATFORMS }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}

17
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: 'Close stale issues'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
days-before-stale: 90
days-before-close: 5
exempt-issue-labels: "bug,enhancement"
exempt-pr-labels: "bug,enhancement"

13
.gitignore vendored
View File

@ -15,6 +15,19 @@
.glide/
# custom
.idea
.vscode
.zed
.DS_Store
# dev test only
/dev/
dev*.go
*_test.go
dist
*.zip
/*.conf
/*.rule

96
.goreleaser.yml Normal file
View File

@ -0,0 +1,96 @@
version: 2
before:
hooks:
- go mod tidy
builds:
- id: default
env:
- CGO_ENABLED=0
goos:
- windows
- linux
- darwin
- freebsd
goarch:
- 386
- amd64
- arm
- arm64
- mips
- mipsle
- mips64
- mips64le
- riscv64
goamd64:
- v1
- v3
goarm:
- 6
- 7
gomips:
- hardfloat
- softfloat
archives:
- id: default
builds:
- default
wrap_in_directory: true
formats: tar.gz
format_overrides:
- goos: windows
formats: zip
files:
- LICENSE
- README.md
- config/**/*
- systemd/*
snapshot:
version_template: '{{ incpatch .Version }}-dev-{{.ShortCommit}}'
checksum:
name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"
release:
prerelease: true
draft: true
nfpms:
- id: glider
package_name: glider
vendor: nadoo
homepage: https://github.com/nadoo/glider
maintainer: nadoo
description: Glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset management features(like dnsmasq).
license: GPL-3.0 License
formats:
# - apk
- deb
# - rpm
dependencies:
- libsystemd0
bindir: /usr/bin
release: 1
epoch: 1
version_metadata: git
section: default
priority: extra
contents:
- src: systemd/glider@.service
dst: /etc/systemd/system/glider@.service
- src: config/glider.conf.example
dst: /etc/glider/glider.conf.example
scripts:
postinstall: "systemd/postinstall.sh"
preremove: "systemd/preremove.sh"
postremove: "systemd/postremove.sh"
deb:
triggers:
interest_noawait:
- /lib/systemd/systemd

View File

@ -1,7 +0,0 @@
language: go
go:
- "1.9.x"
- "1.10.x"
- master

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
# Build Stage
FROM golang:1.24-alpine AS build-env
ADD . /src
RUN apk --no-cache add git \
&& cd /src && go build -v -ldflags "-s -w"
# Final Stage
FROM alpine
COPY --from=build-env /src/glider /app/
WORKDIR /app
RUN apk -U upgrade --no-cache \
&& apk --no-cache add ca-certificates
USER 1000
ENTRYPOINT ["./glider"]

616
README.md
View File

@ -1,213 +1,431 @@
# [glider](https://github.com/nadoo/glider)
[![Build Status](https://img.shields.io/travis/nadoo/glider.svg?style=flat-square)](https://travis-ci.org/nadoo/glider)
[![Go Version](https://img.shields.io/github/go-mod/go-version/nadoo/glider?style=flat-square)](https://go.dev/dl/)
[![Go Report Card](https://goreportcard.com/badge/github.com/nadoo/glider?style=flat-square)](https://goreportcard.com/report/github.com/nadoo/glider)
[![GitHub tag](https://img.shields.io/github/tag/nadoo/glider.svg?style=flat-square)](https://github.com/nadoo/glider/releases)
[![GitHub release](https://img.shields.io/github/release/nadoo/glider.svg?style=flat-square)](https://github.com/nadoo/glider/releases)
[![GitHub release](https://img.shields.io/github/v/release/nadoo/glider.svg?style=flat-square&include_prereleases)](https://github.com/nadoo/glider/releases)
[![Actions Status](https://img.shields.io/github/actions/workflow/status/nadoo/glider/build.yml?branch=dev&style=flat-square)](https://github.com/nadoo/glider/actions)
[![DockerHub](https://img.shields.io/docker/image-size/nadoo/glider?color=blue&label=docker&style=flat-square)](https://hub.docker.com/r/nadoo/glider)
glider is a forward proxy with multiple protocols support, and also a dns forwarding server with ipset management features(like dnsmasq).
glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset management features(like dnsmasq).
we can set up local listeners as proxy servers, and forward requests to internet via forwarders.
```
```bash
|Forwarder ----------------->|
Listener --> | | Internet
|Forwarder --> Forwarder->...|
```
## Features
Listen (local proxy server):
- Socks5 proxy(tcp&udp)
- Http proxy(tcp)
- SS proxy(tcp&udp)
- Linux transparent proxy(iptables redirect)
- TCP tunnel
- UDP tunnel
- UDP over TCP tunnel
- DNS Tunnel(udp2tcp)
- Act as both proxy client and proxy server(protocol converter)
- Flexible proxy & protocol chains
- Load balancing with the following scheduling algorithm:
- rr: round robin
- ha: high availability
- lha: latency based high availability
- dh: destination hashing
- Rule & priority based forwarder choosing: [Config Examples](config/examples)
- DNS forwarding server:
- dns over proxy
- force upstream querying by tcp
- association rules between dns and forwarder choosing
- association rules between dns and ipset
- dns cache support
- custom dns record
- IPSet management (linux kernel version >= 2.6.32):
- add ip/cidrs from rule files on startup
- add resolved ips for domains from rule files by dns forwarding server
- Serve http and socks5 on the same port
- Periodical availability checking for forwarders
- Send requests from specific local ip/interface
- Services:
- dhcpd: a simple dhcp server that can run in failover mode
Forward (local proxy client/upstream proxy server):
- Socks5 proxy(tcp&udp)
- Http proxy(tcp)
- SS proxy(tcp&udp&uot)
- SSR proxy(tcp)
- VMess proxy(tcp)
- TLS, use it together with above proxy protocols(tcp)
## Protocols
DNS Forwarding Server (udp2tcp):
- Listen on UDP and forward dns requests to remote dns server in TCP via forwarders
- Specify different upstream dns server based on destinations(in rule file)
- Tunnel mode: forward to a fixed upstream dns server
- Add resolved IPs to proxy rules
- Add resolved IPs to ipset
<details>
<summary>click to see details</summary>
IPSet Management:
- Add ip/cidrs from rule files on startup
- Add resolved ips for domains from rule files by dns forwarding server
|Protocol | Listen/TCP | Listen/UDP | Forward/TCP | Forward/UDP | Description
|:-: |:-:|:-:|:-:|:-:|:-
|Mixed |√|√| | |http+socks5 server
|HTTP |√| |√| |client & server
|SOCKS5 |√|√|√|√|client & server
|SS |√|√|√|√|client & server
|Trojan |√|√|√|√|client & server
|Trojanc |√|√|√|√|trojan cleartext(without tls)
|VLESS |√|√|√|√|client & server
|VMess | | |√|√|client only
|SSR | | |√| |client only
|SSH | | |√| |client only
|SOCKS4 | | |√| |client only
|SOCKS4A | | |√| |client only
|TCP |√| |√| |tcp tunnel client & server
|UDP | |√| |√|udp tunnel client & server
|TLS |√| |√| |transport client & server
|KCP | |√|√| |transport client & server
|Unix |√|√|√|√|transport client & server
|VSOCK |√| |√| |transport client & server
|Smux |√| |√| |transport client & server
|Websocket(WS) |√| |√| |transport client & server
|WS Secure |√| |√| |websocket secure (wss)
|Proxy Protocol |√| | | |version 1 server only
|Simple-Obfs | | |√| |transport client only
|Redir |√| | | |linux redirect proxy
|Redir6 |√| | | |linux redirect proxy(ipv6)
|TProxy | |√| | |linux tproxy(udp only)
|Reject | | |√|√|reject all requests
General:
- Http and socks5 on the same port
- Forward chain
- HA or RR strategy for multiple forwarders
- Periodical proxy checking
- Rule proxy based on destinations: [Config Examples](config/examples)
TODO:
- [ ] Transparent UDP proxy (iptables tproxy)
- [ ] DNS Cache
- [ ] Performance tuning
- [ ] TUN/TAP device support
- [ ] IPv6 support
- [ ] SSH tunnel support
</details>
## Install
Binary:
- [https://github.com/nadoo/glider/releases](https://github.com/nadoo/glider/releases)
Go Get (requires **Go 1.9+** ):
```bash
go get -u github.com/nadoo/glider
```
ArchLinux:
```bash
sudo pacman -S glider
```
## Run
command line:
```bash
glider -listen :8443 -verbose
```
config file:
```bash
glider -config CONFIGPATH
```
command line with config file:
```bash
glider -config CONFIGPATH -listen :8080 -verbose
```
- Binary: [https://github.com/nadoo/glider/releases](https://github.com/nadoo/glider/releases)
- Docker: `docker pull nadoo/glider`
- Manjaro: `pamac install glider`
- ArchLinux: `sudo pacman -S glider`
- Homebrew: `brew install glider`
- MacPorts: `sudo port install glider`
- Source: `go install github.com/nadoo/glider@latest`
## Usage
#### Run
```bash
glider v0.6.0 usage:
-checkduration int
proxy check duration(seconds) (default 30)
-checkwebsite string
proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80 (default "www.apple.com")
glider -verbose -listen :8443
# docker run --rm -it nadoo/glider -verbose -listen :8443
```
#### Help
<details>
<summary><code>glider -help</code></summary>
```bash
Usage: glider [-listen URL]... [-forward URL]... [OPTION]...
e.g. glider -config /etc/glider/glider.conf
glider -listen :8443 -forward socks5://serverA:1080 -forward socks5://serverB:1080 -verbose
OPTION:
-check string
check=tcp[://HOST:PORT]: tcp port connect check
check=http://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
check=https://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
check=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, env vars: FORWARDER_ADDR,FORWARDER_URL
check=disable: disable health check (default "http://www.msftconnecttest.com/connecttest.txt#expect=200")
-checkdisabledonly
check disabled fowarders only
-checkinterval int
fowarder check interval(seconds) (default 30)
-checklatencysamples int
use the average latency of the latest N checks (default 10)
-checktimeout int
fowarder check timeout(seconds) (default 10)
-checktolerance int
fowarder check tolerance(ms), switch only when new_latency < old_latency - tolerance, only used in lha mode
-config string
config file path
-dialtimeout int
dial timeout(seconds) (default 3)
-dns string
dns forwarder server listen address
local dns server listen address
-dnsalwaystcp
always use tcp to query upstream dns servers no matter there is a forwarder or not
-dnscachelog
show query log of dns cache
-dnscachesize int
max number of dns response in CACHE (default 4096)
-dnsmaxttl int
maximum TTL value for entries in the CACHE(seconds) (default 1800)
-dnsminttl int
minimum TTL value for entries in the CACHE(seconds)
-dnsnoaaaa
disable AAAA query
-dnsrecord value
custom dns record, format: domain/ip
-dnsserver value
remote dns server
remote dns server address
-dnstimeout int
timeout value used in multiple dnsservers switch(seconds) (default 3)
-example
show usage examples
-forward value
forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS]
-ipset string
ipset name
forward url, see the URL section below
-include value
include file
-interface string
source ip or source interface
-listen value
listen url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS
listen url, see the URL section below
-logflags int
do not change it if you do not know what it is, ref: https://pkg.go.dev/log#pkg-constants (default 19)
-maxfailures int
max failures to change forwarder status to disabled (default 3)
-relaytimeout int
relay timeout(seconds)
-rulefile value
rule file path
-rules-dir string
rule file folder
-scheme string
show help message of proxy scheme, use 'all' to see all schemes
-service value
run specified services, format: SERVICE_NAME[,SERVICE_CONFIG]
-strategy string
forward strategy, default: rr (default "rr")
rr: Round Robin mode
ha: High Availability mode
lha: Latency based High Availability mode
dh: Destination Hashing mode (default "rr")
-tcpbufsize int
tcp buffer size in Bytes (default 32768)
-udpbufsize int
udp buffer size in Bytes (default 2048)
-verbose
verbose mode
Available Schemes:
mixed: serve as a http/socks5 proxy on the same port. (default)
ss: ss proxy
socks5: socks5 proxy
http: http proxy
ssr: ssr proxy
vmess: vmess proxy
redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules)
tcptun: tcp tunnel
udptun: udp tunnel
uottun: udp over tcp tunnel
dnstun: listen on udp port and forward all dns requests to remote dns server via forwarders(tcp)
URL:
proxy: SCHEME://[USER:PASS@][HOST]:PORT
chain: proxy,proxy[,proxy]...
Available schemes for different modes:
listen: mixed ss socks5 http redir tcptun udptun uottun dnstun
forward: ss socks5 http ssr vmess
e.g. -listen socks5://:1080
-listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// (protocol chain)
e.g. -forward socks5://server:1080
-forward tls://server.com:443,http:// (protocol chain)
-forward socks5://serverA:1080,socks5://serverB:1080 (proxy chain)
SCHEME:
listen : http kcp mixed pxyproto redir redir6 smux sni socks5 ss tcp tls tproxy trojan trojanc udp unix vless vsock ws wss
forward: direct http kcp reject simple-obfs smux socks4 socks4a socks5 ss ssh ssr tcp tls trojan trojanc udp unix vless vmess vsock ws wss
Note: use 'glider -scheme all' or 'glider -scheme SCHEME' to see help info for the scheme.
--
Forwarder Options: FORWARD_URL#OPTIONS
priority : the priority of that forwarder, the larger the higher, default: 0
interface: the local interface or ip address used to connect remote server.
e.g. -forward socks5://server:1080#priority=100
-forward socks5://server:1080#interface=eth0
-forward socks5://server:1080#priority=100&interface=192.168.1.99
Services:
dhcpd: service=dhcpd,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
service=dhcpd-failover,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
e.g. service=dhcpd,eth1,192.168.1.100,192.168.1.199,720
--
Help:
glider -help
glider -scheme all
glider -example
see README.md and glider.conf.example for more details.
--
glider 0.16.4, https://github.com/nadoo/glider (glider.proxy@gmail.com)
```
</details>
#### Schemes
<details>
<summary><code>glider -scheme all</code></summary>
```bash
Direct scheme:
direct://
Only needed when you want to specify the outgoing interface:
glider -verbose -listen :8443 -forward direct://#interface=eth0
Or load balance multiple interfaces directly:
glider -verbose -listen :8443 -forward direct://#interface=eth0 -forward direct://#interface=eth1 -strategy rr
Or you can use the high availability mode:
glider -verbose -listen :8443 -forward direct://#interface=eth0&priority=100 -forward direct://#interface=eth1&priority=200 -strategy ha
--
Http scheme:
http://[user:pass@]host:port
--
KCP scheme:
kcp://CRYPT:KEY@host:port[?dataShards=NUM&parityShards=NUM&mode=MODE]
Available crypt types for KCP:
none, sm4, tea, xor, aes, aes-128, aes-192, blowfish, twofish, cast5, 3des, xtea, salsa20
Available modes for KCP:
fast, fast2, fast3, normal, default: fast
--
Simple-Obfs scheme:
simple-obfs://host:port[?type=TYPE&host=HOST&uri=URI&ua=UA]
Available types for simple-obfs:
http, tls
--
Reject scheme:
reject://
--
Smux scheme:
smux://host:port
--
Socks4 scheme:
socks4://host:port
--
Socks5 scheme:
socks5://[user:pass@]host:port
--
SS scheme:
ss://method:pass@host:port
Available methods for ss:
AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20
NOTE: chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305
AEAD Ciphers:
AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AEAD_XCHACHA20_POLY1305
Stream Ciphers:
AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20 CHACHA20 RC4-MD5
Alias:
chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305, xchacha20-ietf-poly1305 = AEAD_XCHACHA20_POLY1305
Plain: NONE
--
SSH scheme:
ssh://user[:pass]@host:port[?key=keypath&timeout=SECONDS]
timeout: timeout of ssh handshake and channel operation, default: 5
--
SSR scheme:
ssr://method:pass@host:port?protocol=xxx&protocol_param=yyy&obfs=zzz&obfs_param=xyz
--
TLS client scheme:
tls://host:port[?serverName=SERVERNAME][&skipVerify=true][&cert=PATH][&alpn=proto1][&alpn=proto2]
Proxy over tls client:
tls://host:port[?skipVerify=true][&serverName=SERVERNAME],scheme://
tls://host:port[?skipVerify=true],http://[user:pass@]
tls://host:port[?skipVerify=true],socks5://[user:pass@]
tls://host:port[?skipVerify=true],vmess://[security:]uuid@?alterID=num
TLS server scheme:
tls://host:port?cert=PATH&key=PATH[&alpn=proto1][&alpn=proto2]
Proxy over tls server:
tls://host:port?cert=PATH&key=PATH,scheme://
tls://host:port?cert=PATH&key=PATH,http://
tls://host:port?cert=PATH&key=PATH,socks5://
tls://host:port?cert=PATH&key=PATH,ss://method:pass@
--
Trojan client scheme:
trojan://pass@host:port[?serverName=SERVERNAME][&skipVerify=true][&cert=PATH]
trojanc://pass@host:port (cleartext, without TLS)
Trojan server scheme:
trojan://pass@host:port?cert=PATH&key=PATH[&fallback=127.0.0.1]
trojanc://pass@host:port[?fallback=127.0.0.1] (cleartext, without TLS)
--
Unix domain socket scheme:
unix://path
--
VLESS scheme:
vless://uuid@host:port[?fallback=127.0.0.1:80]
--
VMess scheme:
vmess://[security:]uuid@host:port?alterID=num
vmess://[security:]uuid@host:port[?alterID=num]
if alterID=0 or not set, VMessAEAD will be enabled
Available methods for vmess:
NONE, (will add aes-128-gcm and chacha20-poly1305 later)
Available security for vmess:
zero, none, aes-128-gcm, chacha20-poly1305
Available forward strategies:
rr: Round Robin mode
ha: High Availability mode
--
Websocket client scheme:
ws://host:port[/path][?host=HOST][&origin=ORIGIN]
wss://host:port[/path][?serverName=SERVERNAME][&skipVerify=true][&cert=PATH][&host=HOST][&origin=ORIGIN]
Config file format(see `glider.conf.example` as an example):
# COMMENT LINE
KEY=VALUE
KEY=VALUE
# KEY equals to command line flag name: listen forward strategy...
Websocket server scheme:
ws://:port[/path][?host=HOST]
wss://:port[/path]?cert=PATH&key=PATH[?host=HOST]
Websocket with a specified proxy protocol:
ws://host:port[/path][?host=HOST],scheme://
ws://host:port[/path][?host=HOST],http://[user:pass@]
ws://host:port[/path][?host=HOST],socks5://[user:pass@]
TLS and Websocket with a specified proxy protocol:
tls://host:port[?skipVerify=true][&serverName=SERVERNAME],ws://[@/path[?host=HOST]],scheme://
tls://host:port[?skipVerify=true],ws://[@/path[?host=HOST]],http://[user:pass@]
tls://host:port[?skipVerify=true],ws://[@/path[?host=HOST]],socks5://[user:pass@]
tls://host:port[?skipVerify=true],ws://[@/path[?host=HOST]],vmess://[security:]uuid@?alterID=num
--
VM socket scheme(linux only):
vsock://[CID]:port
if you want to listen on any address, just set CID to 4294967295.
```
</details>
#### Examples
<details>
<summary><code>glider -example</code></summary>
```bash
Examples:
glider -config glider.conf
-run glider with specified config file.
glider -config glider.conf -rulefile office.rule -rulefile home.rule
-run glider with specified global config file and rule config files.
glider -listen :8443 -verbose
-listen on :8443, serve as http/socks5 proxy on the same port, in verbose mode.
glider -listen :8443
-listen on :8443, serve as http/socks5 proxy on the same port.
glider -listen socks5://:1080 -listen http://:8080 -verbose
-multiple listeners: listen on :1080 as socks5 proxy server, and on :8080 as http proxy server.
glider -listen ss://AEAD_CHACHA20_POLY1305:pass@:8443
-listen on 0.0.0.0:8443 as a ss server.
glider -listen :8443 -forward direct://#interface=eth0 -forward direct://#interface=eth1
-multiple forwarders: listen on 8443 and forward requests via interface eth0 and eth1 in round robin mode.
glider -listen socks5://:1080 -verbose
-listen on :1080 as a socks5 proxy server, in verbose mode.
glider -listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// -verbose
-protocol chain: listen on :443 as a https(http over tls) proxy server.
glider -listen http://:8080 -forward socks5://127.0.0.1:1080
-listen on :8080 as a http proxy server, forward all requests via socks5 server.
glider -listen http://:8080 -forward socks5://serverA:1080,socks5://serverB:1080
-proxy chain: listen on :8080 as a http proxy server, forward all requests via forward chain.
glider -listen redir://:1081 -forward ss://method:pass@1.1.1.1:8443
-listen on :1081 as a transparent redirect server, forward all requests via remote ss server.
glider -listen :8443 -forward socks5://serverA:1080 -forward socks5://serverB:1080#priority=10 -forward socks5://serverC:1080#priority=10
-forwarder priority: serverA will only be used when serverB and serverC are not available.
glider -listen redir://:1081 -forward "ssr://method:pass@1.1.1.1:8444?protocol=a&protocol_param=b&obfs=c&obfs_param=d"
-listen on :1081 as a transparent redirect server, forward all requests via remote ssr server.
glider -listen tcp://:80 -forward tcp://serverA:80
-tcp tunnel: listen on :80 and forward all requests to serverA:80.
glider -listen redir://:1081 -forward "tls://1.1.1.1:443,vmess://user:method@?alterID=10"
-listen on :1081 as a transparent redirect server, forward all requests via remote vmess server.
glider -listen udp://:53 -forward socks5://serverA:1080,udp://8.8.8.8:53
-udp tunnel: listen on :53 and forward all udp requests to 8.8.8.8:53 via remote socks5 server.
glider -listen tcptun://:80=2.2.2.2:80 -forward ss://method:pass@1.1.1.1:8443
-listen on :80 and forward all requests to 2.2.2.2:80 via remote ss server.
glider -listen udptun://:53=8.8.8.8:53 -forward ss://method:pass@1.1.1.1:8443
-listen on :53 and forward all udp requests to 8.8.8.8:53 via remote ss server.
glider -listen uottun://:53=8.8.8.8:53 -forward ss://method:pass@1.1.1.1:8443
-listen on :53 and forward all udp requests via udp over tcp tunnel.
glider -listen socks5://:1080 -listen http://:8080 -forward ss://method:pass@1.1.1.1:8443
-listen on :1080 as socks5 server, :8080 as http proxy server, forward all requests via remote ss server.
glider -listen redir://:1081 -listen dnstun://:53=8.8.8.8:53 -forward ss://method:pass@server1:port1,ss://method:pass@server2:port2
-listen on :1081 as transparent redirect server, :53 as dns server, use forward chain: server1 -> server2.
glider -listen socks5://:1080 -forward ss://method:pass@server1:port1 -forward ss://method:pass@server2:port2 -strategy rr
-listen on :1080 as socks5 server, forward requests via server1 and server2 in round robin mode.
glider -verbose -dns=:53 -dnsserver=8.8.8.8:53 -forward socks5://serverA:1080 -dnsrecord=abc.com/1.2.3.4
-dns over proxy: listen on :53 as dns server, forward to 8.8.8.8:53 via socks5 server.
```
</details>
## Config
```bash
glider -config CONFIG_PATH
```
## Advanced Usage
- [ConfigFile](config)
- [glider.conf.example](config/glider.conf.example)
- [office.rule.example](config/rules.d/office.rule.example)
@ -216,9 +434,109 @@ Examples:
- [transparent proxy without dnsmasq](config/examples/9.transparent_proxy_without_dnsmasq)
## Service
- systemd: [https://github.com/nadoo/glider/blob/master/systemd/](https://github.com/nadoo/glider/blob/master/systemd/)
- dhcpd / dhcpd-failover:
- service=dhcpd,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
- service=dhcpd,eth1,192.168.1.100,192.168.1.199,720,fc:23:34:9e:25:01=192.168.1.101
- service=dhcpd-failover,eth2,192.168.2.100,192.168.2.199,720
- note: `dhcpd-failover` only serves requests when there's no other dhcp server exists in lan
- detect interval: 1min
## Linux Daemon
- systemd: [https://github.com/nadoo/glider/tree/main/systemd](https://github.com/nadoo/glider/tree/main/systemd)
- <details> <summary>docker: click to see details</summary>
- run glider (config file path: /etc/glider/glider.conf)
```
docker run -d --name glider --net host --restart=always \
-v /etc/glider:/etc/glider \
-v /etc/localtime:/etc/localtime:ro \
nadoo/glider -config=/etc/glider/glider.conf
```
- run watchtower if you need auto update
```
docker run -d --name watchtower --restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower --interval 21600 --cleanup \
glider
```
- open udp ports if you need udp nat fullcone
```
iptables -I INPUT -p udp -m udp --dport 1024:65535 -j ACCEPT
```
</details>
## Customize Build
<details><summary>You can customize and build glider if you want a smaller binary (click to see details)</summary>
1. Clone the source code:
```bash
git clone https://github.com/nadoo/glider && cd glider
```
2. Customize features:
```bash
open `feature.go` & `feature_linux.go`, comment out the packages you don't need
// _ "github.com/nadoo/glider/proxy/kcp"
```
3. Build it:
```bash
go build -v -ldflags "-s -w"
```
</details>
## Proxy & Protocol Chains
<details><summary>In glider, you can easily chain several proxy servers or protocols together (click to see details)</summary>
- Chain proxy servers:
```bash
forward=http://1.1.1.1:80,socks5://2.2.2.2:1080,ss://method:pass@3.3.3.3:8443@
```
- Chain protocols: https proxy (http over tls)
```bash
forward=tls://server.com:443,http://
```
- Chain protocols: vmess over ws over tls
```bash
forward=tls://server.com:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
```
- Chain protocols and servers:
``` bash
forward=socks5://1.1.1.1:1080,tls://server.com:443,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
```
- Chain protocols in listener: https proxy server
``` bash
listen=tls://:443?cert=crtFilePath&key=keyFilePath,http://
```
- Chain protocols in listener: http over smux over websocket proxy server
``` bash
listen=ws://:10000,smux://,http://
```
</details>
## Links
- [go-ss2](https://github.com/shadowsocks/go-shadowsocks2): ss protocol support
- [conflag](https://github.com/nadoo/conflag): command line and config file parse support
- [ArchLinux](https://www.archlinux.org/packages/community/x86_64/glider): a great linux distribution with glider pre-built package
- [ipset](https://github.com/nadoo/ipset): netlink ipset package for Go.
- [conflag](https://github.com/nadoo/conflag): a drop-in replacement for Go's standard flag package with config file support.
- [ArchLinux](https://archlinux.org/packages/extra/x86_64/glider): a great linux distribution with glider pre-built package.
- [urlencode](https://www.w3schools.com/tags/ref_urlencode.asp): you should encode special characters in scheme url. e.g., `@`->`%40`

View File

@ -1,94 +0,0 @@
package conn
import (
"bufio"
"io"
"net"
"time"
"github.com/nadoo/glider/common/log"
)
// UDPBufSize is the size of udp buffer
const UDPBufSize = 65536
// Conn struct
type Conn struct {
r *bufio.Reader
net.Conn
}
// NewConn .
func NewConn(c net.Conn) Conn {
return Conn{bufio.NewReader(c), c}
}
// NewConnSize .
func NewConnSize(c net.Conn, n int) Conn {
return Conn{bufio.NewReaderSize(c, n), c}
}
// Peek .
func (c Conn) Peek(n int) ([]byte, error) {
return c.r.Peek(n)
}
// Read .
func (c Conn) Read(p []byte) (int, error) {
return c.r.Read(p)
}
// Relay .
func Relay(left, right net.Conn) (int64, int64, error) {
type res struct {
N int64
Err error
}
ch := make(chan res)
go func() {
n, err := io.Copy(right, left)
right.SetDeadline(time.Now()) // wake up the other goroutine blocking on right
left.SetDeadline(time.Now()) // wake up the other goroutine blocking on left
ch <- res{n, err}
}()
n, err := io.Copy(left, right)
right.SetDeadline(time.Now()) // wake up the other goroutine blocking on right
left.SetDeadline(time.Now()) // wake up the other goroutine blocking on left
rs := <-ch
if err == nil {
err = rs.Err
}
return n, rs.N, err
}
// TimedCopy copy from src to dst at target with read timeout
func TimedCopy(dst net.PacketConn, target net.Addr, src net.PacketConn, timeout time.Duration) error {
buf := make([]byte, UDPBufSize)
for {
src.SetReadDeadline(time.Now().Add(timeout))
n, _, err := src.ReadFrom(buf)
if err != nil {
return err
}
_, err = dst.WriteTo(buf[:n], target)
if err != nil {
return err
}
}
}
// OutboundIP returns preferred outbound ip of this machine
func OutboundIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
log.F("get outbound ip error: %s", err)
return ""
}
defer conn.Close()
return conn.LocalAddr().(*net.UDPAddr).IP.String()
}

View File

@ -1,19 +0,0 @@
package log
import stdlog "log"
// Func defines a simple log function
type Func func(f string, v ...interface{})
// F is the main log function
var F Func = func(string, ...interface{}) {}
// Fatal log and exit
func Fatal(v ...interface{}) {
stdlog.Fatal(v...)
}
// Fatalf log and exit
func Fatalf(f string, v ...interface{}) {
stdlog.Fatalf(f, v...)
}

236
conf.go
View File

@ -1,236 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path"
"github.com/nadoo/conflag"
)
var flag = conflag.New()
var conf struct {
Verbose bool
Strategy string
CheckWebSite string
CheckDuration int
Listen []string
Forward []string
RuleFile []string
RulesDir string
DNS string
DNSServer []string
IPSet string
rules []*RuleConf
}
func confInit() {
flag.BoolVar(&conf.Verbose, "verbose", false, "verbose mode")
flag.StringVar(&conf.Strategy, "strategy", "rr", "forward strategy, default: rr")
flag.StringVar(&conf.CheckWebSite, "checkwebsite", "www.apple.com", "proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80")
flag.IntVar(&conf.CheckDuration, "checkduration", 30, "proxy check duration(seconds)")
flag.StringSliceUniqVar(&conf.Listen, "listen", nil, "listen url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS")
flag.StringSliceUniqVar(&conf.Forward, "forward", nil, "forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS]")
flag.StringSliceUniqVar(&conf.RuleFile, "rulefile", nil, "rule file path")
flag.StringVar(&conf.RulesDir, "rules-dir", "", "rule file folder")
flag.StringVar(&conf.DNS, "dns", "", "dns forwarder server listen address")
flag.StringSliceUniqVar(&conf.DNSServer, "dnsserver", []string{"8.8.8.8:53"}, "remote dns server")
flag.StringVar(&conf.IPSet, "ipset", "", "ipset name")
flag.Usage = usage
err := flag.Parse()
if err != nil {
flag.Usage()
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
os.Exit(-1)
}
if len(conf.Listen) == 0 && conf.DNS == "" {
flag.Usage()
fmt.Fprintf(os.Stderr, "ERROR: listen url must be specified.\n")
os.Exit(-1)
}
// rulefiles
for _, ruleFile := range conf.RuleFile {
rule, err := NewRuleConfFromFile(ruleFile)
if err != nil {
log.Fatal(err)
}
conf.rules = append(conf.rules, rule)
}
if conf.RulesDir != "" {
conf.RulesDir = path.Join(flag.ConfDir(), conf.RulesDir)
ruleFolderFiles, _ := listDir(conf.RulesDir, ".rule")
for _, ruleFile := range ruleFolderFiles {
rule, err := NewRuleConfFromFile(ruleFile)
if err != nil {
log.Fatal(err)
}
conf.rules = append(conf.rules, rule)
}
}
}
// RuleConf , every ruleForwarder points to a rule file
type RuleConf struct {
name string
Forward []string
Strategy string
CheckWebSite string
CheckDuration int
DNSServer []string
IPSet string
Domain []string
IP []string
CIDR []string
}
// NewRuleConfFromFile .
func NewRuleConfFromFile(ruleFile string) (*RuleConf, error) {
p := &RuleConf{name: ruleFile}
f := conflag.NewFromFile("rule", ruleFile)
f.StringSliceUniqVar(&p.Forward, "forward", nil, "forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS]")
f.StringVar(&p.Strategy, "strategy", "rr", "forward strategy, default: rr")
f.StringVar(&p.CheckWebSite, "checkwebsite", "www.apple.com", "proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80")
f.IntVar(&p.CheckDuration, "checkduration", 30, "proxy check duration(seconds)")
f.StringSliceUniqVar(&p.DNSServer, "dnsserver", nil, "remote dns server")
f.StringVar(&p.IPSet, "ipset", "", "ipset name")
f.StringSliceUniqVar(&p.Domain, "domain", nil, "domain")
f.StringSliceUniqVar(&p.IP, "ip", nil, "ip")
f.StringSliceUniqVar(&p.CIDR, "cidr", nil, "cidr")
err := f.Parse()
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
return nil, err
}
return p, err
}
func usage() {
app := os.Args[0]
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "%s v%s usage:\n", app, VERSION)
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Available Schemes:\n")
fmt.Fprintf(os.Stderr, " mixed: serve as a http/socks5 proxy on the same port. (default)\n")
fmt.Fprintf(os.Stderr, " ss: ss proxy\n")
fmt.Fprintf(os.Stderr, " socks5: socks5 proxy\n")
fmt.Fprintf(os.Stderr, " http: http proxy\n")
fmt.Fprintf(os.Stderr, " ssr: ssr proxy\n")
fmt.Fprintf(os.Stderr, " vmess: vmess proxy\n")
fmt.Fprintf(os.Stderr, " redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules)\n")
fmt.Fprintf(os.Stderr, " tcptun: tcp tunnel\n")
fmt.Fprintf(os.Stderr, " udptun: udp tunnel\n")
fmt.Fprintf(os.Stderr, " uottun: udp over tcp tunnel\n")
fmt.Fprintf(os.Stderr, " dnstun: listen on udp port and forward all dns requests to remote dns server via forwarders(tcp)\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Available schemes for different modes:\n")
fmt.Fprintf(os.Stderr, " listen: mixed ss socks5 http redir tcptun udptun uottun dnstun\n")
fmt.Fprintf(os.Stderr, " forward: ss socks5 http ssr vmess\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "SS scheme:\n")
fmt.Fprintf(os.Stderr, " ss://method:pass@host:port\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Available methods for ss:\n")
fmt.Fprintf(os.Stderr, " AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " NOTE: chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "SSR scheme:\n")
fmt.Fprintf(os.Stderr, " ssr://method:pass@host:port?protocol=xxx&protocol_param=yyy&obfs=zzz&obfs_param=xyz\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "VMess scheme:\n")
fmt.Fprintf(os.Stderr, " vmess://[security:]uuid@host:port?alterID=num\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Available methods for vmess:\n")
fmt.Fprintf(os.Stderr, " NONE, (will add aes-128-gcm and chacha20-poly1305 later)\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Available forward strategies:\n")
fmt.Fprintf(os.Stderr, " rr: Round Robin mode\n")
fmt.Fprintf(os.Stderr, " ha: High Availability mode\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Config file format(see `"+app+".conf.example` as an example):\n")
fmt.Fprintf(os.Stderr, " # COMMENT LINE\n")
fmt.Fprintf(os.Stderr, " KEY=VALUE\n")
fmt.Fprintf(os.Stderr, " KEY=VALUE\n")
fmt.Fprintf(os.Stderr, " # KEY equals to command line flag name: listen forward strategy...\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Examples:\n")
fmt.Fprintf(os.Stderr, " "+app+" -config glider.conf\n")
fmt.Fprintf(os.Stderr, " -run glider with specified config file.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -config glider.conf -rulefile office.rule -rulefile home.rule\n")
fmt.Fprintf(os.Stderr, " -run glider with specified global config file and rule config files.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen :8443\n")
fmt.Fprintf(os.Stderr, " -listen on :8443, serve as http/socks5 proxy on the same port.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen ss://AEAD_CHACHA20_POLY1305:pass@:8443\n")
fmt.Fprintf(os.Stderr, " -listen on 0.0.0.0:8443 as a ss server.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen socks5://:1080 -verbose\n")
fmt.Fprintf(os.Stderr, " -listen on :1080 as a socks5 proxy server, in verbose mode.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen http://:8080 -forward socks5://127.0.0.1:1080\n")
fmt.Fprintf(os.Stderr, " -listen on :8080 as a http proxy server, forward all requests via socks5 server.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -forward ss://method:pass@1.1.1.1:8443\n")
fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote ss server.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -forward \"ssr://method:pass@1.1.1.1:8444?protocol=a&protocol_param=b&obfs=c&obfs_param=d\"\n")
fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote ssr server.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -forward \"tls://1.1.1.1:443,vmess://user:method@?alterID=10\"\n")
fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote vmess server.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen tcptun://:80=2.2.2.2:80 -forward ss://method:pass@1.1.1.1:8443\n")
fmt.Fprintf(os.Stderr, " -listen on :80 and forward all requests to 2.2.2.2:80 via remote ss server.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen udptun://:53=8.8.8.8:53 -forward ss://method:pass@1.1.1.1:8443\n")
fmt.Fprintf(os.Stderr, " -listen on :53 and forward all udp requests to 8.8.8.8:53 via remote ss server.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen uottun://:53=8.8.8.8:53 -forward ss://method:pass@1.1.1.1:8443\n")
fmt.Fprintf(os.Stderr, " -listen on :53 and forward all udp requests via udp over tcp tunnel.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen socks5://:1080 -listen http://:8080 -forward ss://method:pass@1.1.1.1:8443\n")
fmt.Fprintf(os.Stderr, " -listen on :1080 as socks5 server, :8080 as http proxy server, forward all requests via remote ss server.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -listen dnstun://:53=8.8.8.8:53 -forward ss://method:pass@server1:port1,ss://method:pass@server2:port2\n")
fmt.Fprintf(os.Stderr, " -listen on :1081 as transparent redirect server, :53 as dns server, use forward chain: server1 -> server2.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen socks5://:1080 -forward ss://method:pass@server1:port1 -forward ss://method:pass@server2:port2 -strategy rr\n")
fmt.Fprintf(os.Stderr, " -listen on :1080 as socks5 server, forward requests via server1 and server2 in round robin mode.\n")
fmt.Fprintf(os.Stderr, "\n")
}

254
config.go Normal file
View File

@ -0,0 +1,254 @@
package main
import (
"fmt"
"os"
"path"
"github.com/nadoo/conflag"
"github.com/nadoo/glider/dns"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
"github.com/nadoo/glider/rule"
)
var flag = conflag.New()
// Config is global config struct.
type Config struct {
Verbose bool
LogFlags int
TCPBufSize int
UDPBufSize int
Listens []string
Forwards []string
Strategy rule.Strategy
RuleFiles []string
RulesDir string
DNS string
DNSConfig dns.Config
rules []*rule.Config
Services []string
}
func parseConfig() *Config {
conf := &Config{}
flag.SetOutput(os.Stdout)
scheme := flag.String("scheme", "", "show help message of proxy scheme, use 'all' to see all schemes")
example := flag.Bool("example", false, "show usage examples")
flag.BoolVar(&conf.Verbose, "verbose", false, "verbose mode")
flag.IntVar(&conf.LogFlags, "logflags", 19, "do not change it if you do not know what it is, ref: https://pkg.go.dev/log#pkg-constants")
flag.IntVar(&conf.TCPBufSize, "tcpbufsize", 32768, "tcp buffer size in Bytes")
flag.IntVar(&conf.UDPBufSize, "udpbufsize", 2048, "udp buffer size in Bytes")
flag.StringSliceUniqVar(&conf.Listens, "listen", nil, "listen url, see the URL section below")
flag.StringSliceVar(&conf.Forwards, "forward", nil, "forward url, see the URL section below")
flag.StringVar(&conf.Strategy.Strategy, "strategy", "rr", `rr: Round Robin mode
ha: High Availability mode
lha: Latency based High Availability mode
dh: Destination Hashing mode`)
flag.StringVar(&conf.Strategy.Check, "check", "http://www.msftconnecttest.com/connecttest.txt#expect=200",
`check=tcp[://HOST:PORT]: tcp port connect check
check=http://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
check=https://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
check=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, env vars: FORWARDER_ADDR,FORWARDER_URL
check=disable: disable health check`)
flag.IntVar(&conf.Strategy.CheckInterval, "checkinterval", 30, "fowarder check interval(seconds)")
flag.IntVar(&conf.Strategy.CheckTimeout, "checktimeout", 10, "fowarder check timeout(seconds)")
flag.IntVar(&conf.Strategy.CheckTolerance, "checktolerance", 0, "fowarder check tolerance(ms), switch only when new_latency < old_latency - tolerance, only used in lha mode")
flag.IntVar(&conf.Strategy.CheckLatencySamples, "checklatencysamples", 10, "use the average latency of the latest N checks")
flag.BoolVar(&conf.Strategy.CheckDisabledOnly, "checkdisabledonly", false, "check disabled fowarders only")
flag.IntVar(&conf.Strategy.MaxFailures, "maxfailures", 3, "max failures to change forwarder status to disabled")
flag.IntVar(&conf.Strategy.DialTimeout, "dialtimeout", 3, "dial timeout(seconds)")
flag.IntVar(&conf.Strategy.RelayTimeout, "relaytimeout", 0, "relay timeout(seconds)")
flag.StringVar(&conf.Strategy.IntFace, "interface", "", "source ip or source interface")
flag.StringSliceUniqVar(&conf.RuleFiles, "rulefile", nil, "rule file path")
flag.StringVar(&conf.RulesDir, "rules-dir", "", "rule file folder")
// dns configs
flag.StringVar(&conf.DNS, "dns", "", "local dns server listen address")
flag.StringSliceUniqVar(&conf.DNSConfig.Servers, "dnsserver", []string{"8.8.8.8:53"}, "remote dns server address")
flag.BoolVar(&conf.DNSConfig.AlwaysTCP, "dnsalwaystcp", false, "always use tcp to query upstream dns servers no matter there is a forwarder or not")
flag.IntVar(&conf.DNSConfig.Timeout, "dnstimeout", 3, "timeout value used in multiple dnsservers switch(seconds)")
flag.IntVar(&conf.DNSConfig.MaxTTL, "dnsmaxttl", 1800, "maximum TTL value for entries in the CACHE(seconds)")
flag.IntVar(&conf.DNSConfig.MinTTL, "dnsminttl", 0, "minimum TTL value for entries in the CACHE(seconds)")
flag.IntVar(&conf.DNSConfig.CacheSize, "dnscachesize", 4096, "max number of dns response in CACHE")
flag.BoolVar(&conf.DNSConfig.CacheLog, "dnscachelog", false, "show query log of dns cache")
flag.BoolVar(&conf.DNSConfig.NoAAAA, "dnsnoaaaa", false, "disable AAAA query")
flag.StringSliceUniqVar(&conf.DNSConfig.Records, "dnsrecord", nil, "custom dns record, format: domain/ip")
// service configs
flag.StringSliceUniqVar(&conf.Services, "service", nil, "run specified services, format: SERVICE_NAME[,SERVICE_CONFIG]")
flag.Usage = usage
if err := flag.Parse(); err != nil {
// flag.Usage()
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
os.Exit(-1)
}
if *scheme != "" {
fmt.Fprint(flag.Output(), proxy.Usage(*scheme))
os.Exit(0)
}
if *example {
fmt.Fprint(flag.Output(), examples)
os.Exit(0)
}
// setup logger
log.Set(conf.Verbose, conf.LogFlags)
if len(conf.Listens) == 0 && conf.DNS == "" && len(conf.Services) == 0 {
// flag.Usage()
fmt.Fprintf(os.Stderr, "ERROR: listen url must be specified.\n")
os.Exit(-1)
}
// tcpbufsize
if conf.TCPBufSize > 0 {
proxy.TCPBufSize = conf.TCPBufSize
}
// udpbufsize
if conf.UDPBufSize > 0 {
proxy.UDPBufSize = conf.UDPBufSize
}
loadRules(conf)
return conf
}
func loadRules(conf *Config) {
// rulefiles
for _, ruleFile := range conf.RuleFiles {
if !path.IsAbs(ruleFile) {
ruleFile = path.Join(flag.ConfDir(), ruleFile)
}
rule, err := rule.NewConfFromFile(ruleFile)
if err != nil {
log.Fatal(err)
}
conf.rules = append(conf.rules, rule)
}
if conf.RulesDir != "" {
if !path.IsAbs(conf.RulesDir) {
conf.RulesDir = path.Join(flag.ConfDir(), conf.RulesDir)
}
ruleFolderFiles, _ := rule.ListDir(conf.RulesDir, ".rule")
for _, ruleFile := range ruleFolderFiles {
rule, err := rule.NewConfFromFile(ruleFile)
if err != nil {
log.Fatal(err)
}
conf.rules = append(conf.rules, rule)
}
}
}
func usage() {
fmt.Fprint(flag.Output(), usage1)
flag.PrintDefaults()
fmt.Fprintf(flag.Output(), usage2, proxy.ServerSchemes(), proxy.DialerSchemes(), version)
}
var usage1 = `
Usage: glider [-listen URL]... [-forward URL]... [OPTION]...
e.g. glider -config /etc/glider/glider.conf
glider -listen :8443 -forward socks5://serverA:1080 -forward socks5://serverB:1080 -verbose
OPTION:
`
var usage2 = `
URL:
proxy: SCHEME://[USER:PASS@][HOST]:PORT
chain: proxy,proxy[,proxy]...
e.g. -listen socks5://:1080
-listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// (protocol chain)
e.g. -forward socks5://server:1080
-forward tls://server.com:443,http:// (protocol chain)
-forward socks5://serverA:1080,socks5://serverB:1080 (proxy chain)
SCHEME:
listen : %s
forward: %s
Note: use 'glider -scheme all' or 'glider -scheme SCHEME' to see help info for the scheme.
--
Forwarder Options: FORWARD_URL#OPTIONS
priority : the priority of that forwarder, the larger the higher, default: 0
interface: the local interface or ip address used to connect remote server.
e.g. -forward socks5://server:1080#priority=100
-forward socks5://server:1080#interface=eth0
-forward socks5://server:1080#priority=100&interface=192.168.1.99
Services:
dhcpd: service=dhcpd,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
service=dhcpd-failover,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
e.g. service=dhcpd,eth1,192.168.1.100,192.168.1.199,720
--
Help:
glider -help
glider -scheme all
glider -example
see README.md and glider.conf.example for more details.
--
glider %s, https://github.com/nadoo/glider (glider.proxy@gmail.com)
`
var examples = `
Examples:
glider -config glider.conf
-run glider with specified config file.
glider -listen :8443 -verbose
-listen on :8443, serve as http/socks5 proxy on the same port, in verbose mode.
glider -listen socks5://:1080 -listen http://:8080 -verbose
-multiple listeners: listen on :1080 as socks5 proxy server, and on :8080 as http proxy server.
glider -listen :8443 -forward direct://#interface=eth0 -forward direct://#interface=eth1
-multiple forwarders: listen on 8443 and forward requests via interface eth0 and eth1 in round robin mode.
glider -listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// -verbose
-protocol chain: listen on :443 as a https(http over tls) proxy server.
glider -listen http://:8080 -forward socks5://serverA:1080,socks5://serverB:1080
-proxy chain: listen on :8080 as a http proxy server, forward all requests via forward chain.
glider -listen :8443 -forward socks5://serverA:1080 -forward socks5://serverB:1080#priority=10 -forward socks5://serverC:1080#priority=10
-forwarder priority: serverA will only be used when serverB and serverC are not available.
glider -listen tcp://:80 -forward tcp://serverA:80
-tcp tunnel: listen on :80 and forward all requests to serverA:80.
glider -listen udp://:53 -forward socks5://serverA:1080,udp://8.8.8.8:53
-udp tunnel: listen on :53 and forward all udp requests to 8.8.8.8:53 via remote socks5 server.
glider -verbose -dns=:53 -dnsserver=8.8.8.8:53 -forward socks5://serverA:1080 -dnsrecord=abc.com/1.2.3.4
-dns over proxy: listen on :53 as dns server, forward to 8.8.8.8:53 via socks5 server.
`

View File

@ -4,7 +4,15 @@ Command:
```bash
glider -config glider.conf
```
Config file, **just use the command line flag name as the key name**:
Config file, **just use the command line flag name as key name**:
```bash
# COMMENT LINE
KEY=VALUE
KEY=VALUE
# KEY equals to command line flag name: listen forward strategy...
```
Example:
```bash
### glider config file
@ -14,9 +22,6 @@ verbose
# listen on 8443, serve as http/socks5 proxy on the same port.
listen=:8443
# listen on udp port 5353, forward dns requests via tcp protocol
listen=dnstun://:5353=8.8.8.8:53
# upstream forward proxy
forward=socks5://192.168.1.10:1080
@ -26,16 +31,14 @@ forward=ss://method:pass@1.1.1.1:8443
# upstream forward proxy (forward chain)
forward=http://1.1.1.1:8080,socks5://2.2.2.2:1080
# multiple upstream proxies forwad strategy
# multiple upstream proxies forward strategy
strategy=rr
# Used to connect via forwarders, if the host is unreachable, the forwarder
# will be set to disabled.
# MUST be a HTTP website server address, format: HOST[:PORT]. HTTPS NOT SUPPORTED.
checkwebsite=www.apple.com
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
# check duration
checkduration=30
# check interval
checkinterval=30
# Setup a dns forwarding server
@ -43,20 +46,18 @@ dns=:53
# global remote dns server (you can specify different dns server in rule file)
dnsserver=8.8.8.8:53
# Create and manage ipset on linux based on destinations in rule files
# - add ip/cidrs in rule files on startup
# - add resolved ips for domains in rule files by dns forwarder server
# Usually used in transparent proxy mode on linux
ipset=glider
# RULE FILES
rules-dir=rules.d
#rulefile=office.rule
#rulefile=home.rule
# INCLUDE MORE CONFIG FILES
#include=dnsrecord.inc.conf
#include=more.inc.conf
```
See:
- [glider.conf.example](config/glider.conf.example)
- [examples](config/examples)
- [glider.conf.example](glider.conf.example)
- [examples](examples)
## Rule File
Rule file, **same as the config file but specify forwarders based on destinations**:
@ -66,12 +67,20 @@ forward=socks5://192.168.1.10:1080
forward=ss://method:pass@1.1.1.1:8443
forward=http://192.168.2.1:8080,socks5://192.168.2.2:1080
strategy=rr
checkwebsite=www.apple.com
checkduration=30
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkinterval=30
# DNS SERVER for domains in this rule file
dnsserver=208.67.222.222:53
# IPSET MANAGEMENT
# ----------------
# Create and mange ipset on linux based on destinations in rule files
# - add ip/cidrs in rule files on startup
# - add resolved ips for domains in rule files by dns forwarding server
# Usually used in transparent proxy mode on linux
ipset=glider
# YOU CAN SPECIFY DESTINATIONS TO USE THE ABOVE FORWARDERS
# matches abc.com and *.abc.com
domain=abc.com

View File

@ -0,0 +1,7 @@
# intranet
dnsrecord=oa.yourcompany.local/10.0.0.1
dnsrecord=git.yourcompany.local/10.0.0.2
# ad
#dnsrecord=ad.domain/127.0.0.1

View File

@ -12,10 +12,8 @@ forward=http://1.1.1.1:8080
# High Availability mode: ha
strategy=rr
# Used to connect via forwarders, if the host is unreachable, the forwarder
# will be set to disabled.
# MUST be a HTTP website server address, format: HOST[:PORT]. HTTPS NOT SUPPORTED.
checkwebsite=www.apple.com
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
# check duration(seconds)
checkduration=30
# check interval(seconds)
checkinterval=30

View File

@ -8,8 +8,9 @@ forward=http://1.1.1.1:8080
# High Availability mode: ha
strategy=rr
checkwebsite=www.apple.com
checkduration=30
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkinterval=30
# matches abc.com and *.abc.com

View File

@ -13,8 +13,9 @@ forward=http://1.1.1.1:8080
# High Availability mode: ha
strategy=rr
checkwebsite=www.apple.com
checkduration=30
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkinterval=30
# NOTE HERE:

View File

@ -1,17 +1,18 @@
forward=http://forwarder4:8080
forward=http://forwarder1:8080
# first connect forwarder1 then forwarder2 then internet
forward=http://forwarder5:8080,socks6://forwarder3:1080
forward=http://forwarder1:8080,socks5://forwarder2:1080
# Round Robin mode: rr
# High Availability mode: ha
strategy=rr
checkwebsite=www.apple.com
checkduration=30
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkinterval=30
# matches 192.168.0.0/16

View File

@ -10,8 +10,9 @@ forward=http://forwarder2:8080,socks5://forwarder3:1080
# High Availability mode: ha
strategy=rr
checkwebsite=www.apple.com
checkduration=30
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkinterval=30
# matches 172.16.0.0/24

View File

@ -1,22 +1,22 @@
## 8. Transparent Proxy with dnsmasq
#### Setup a redirect proxy and a dnstunnel with glider
#### Setup a redirect proxy and a dns server with glider
glider.conf
```bash
verbose=True
listen=redir://:1081
listen=dnstun://5353=8.8.8.8:53
forward=http://forwarder1:8080,socks5://forwarder2:1080
forward=http://1.1.1.1:8080
dns=:5353
dnsserver=8.8.8.8:53
strategy=rr
checkwebsite=www.apple.com
checkduration=30
checkinterval=30
```
#### Create a ipset manually
```bash
ipset create myset hash:ip
ipset create myset hash:net
```
#### Config dnsmasq
@ -32,7 +32,7 @@ ipset=/example4.com/myset
#### Config iptables on your linux gateway
```bash
iptables -t nat -I PREROUTING -p tcp -m set --match-set myset dst -j REDIRECT --to-ports 1081
iptables -t nat -I OUTPUT -p tcp -m set --match-set myset dst -j REDIRECT --to-ports 1081
#iptables -t nat -I OUTPUT -p tcp -m set --match-set myset dst -j REDIRECT --to-ports 1081
```
#### When client requests network, the whole process:

View File

@ -3,11 +3,15 @@
verbose=True
listen=redir://:1081
listen=dnstun://5353=8.8.8.8:53
forward=http://forwarder1:8080,socks5://forwarder2:1080
forward=http://1.1.1.1:8080
dns=:5353
dnsserver=8.8.8.8:53
strategy=rr
checkwebsite=www.apple.com
checkduration=30
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkinterval=30

View File

@ -26,9 +26,7 @@ listen=redir://:1081
# as a dns forwarding server
dns=:53
dnsserver=8.8.8.8:53
# as a ipset manager
ipset=glider
dnsserver=8.8.4.4:53
# specify rule files
rules-dir=rules.d
@ -40,12 +38,14 @@ rules-dir=rules.d
forward=http://forwarder1:8080,socks5://forwarder2:1080
forward=http://1.1.1.1:8080
strategy=rr
checkwebsite=www.apple.com
checkduration=30
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
# specify a different dns server(if need)
dnsserver=208.67.222.222:53
# as a ipset manager
ipset=glider
# specify destinations
include=office.list
@ -66,8 +66,8 @@ domain=mycompany.com
domain=mycompany1.com
ip=4.4.4.4
ip=5.5.5.5
cidr=cidr=172.16.101.0/24
cidr=cidr=172.16.102.0/24
cidr=172.16.101.0/24
cidr=172.16.102.0/24
```
#### Configure iptables on your linux gateway
@ -76,19 +76,26 @@ iptables -t nat -I PREROUTING -p tcp -m set --match-set glider dst -j REDIRECT -
iptables -t nat -I OUTPUT -p tcp -m set --match-set glider dst -j REDIRECT --to-ports 1081
```
#### Client DNS settings
use the linux server's ip as your dns server
#### Server DNS settings
Set server's nameserver to glider:
```bash
echo nameserver 127.0.0.1 > /etc/resolv.conf
```
#### Client settings
Use the linux server's ip as your gateway.
Use the linux server's ip as your dns server.
#### When client requesting to access http://example1.com (in office.rule), the whole process:
DNS Resolving:
1. client sends a udp dns request to linux server, and glider will receive the request(as it listen on default dns port :53)
2. upstream dns server choice: glider will lookup it's rule config and find out the dns server to use for this domain(matched "example1.com" in office.rule, so 208.67.222.222:53 will be choosen)
3. glider uses the forwarder in office.rule to ask 208.67.222.222:53 for the resolve answers
4. glider updates it's office rule config, add the resolved ip address to it
5. glider adds the resolved ip into ipset "glider", and return the dns answer to client
1. client sends a udp dns request to linux server, and glider will receive the request(as it listens on the default dns port :53)
2. upstream dns server choice: glider will lookup it's rule config and find out the dns server to use for this domain(matched "example1.com" in office.rule, so 208.67.222.222:53 will be chosen)
3. glider uses the forwarder in office.rule to ask 208.67.222.222:53 for the resolve answers(dns over proxy).
4. glider updates it's office rule config, adds the resolved ip address to it.
5. glider adds the resolved ip into ipset "glider", and returns the dns answer to client.
Destination Accessing:
1. client sends http request to the resolved ip of example1.com
2. linux gateway server will get the request
3. iptabes matches the ip in ipset "glider" and redirect this request to :1081(glider)
4. glider finds the ip in office rule, and then choose a forwarder in office.rule to complete the request
1. client sends http request to the resolved ip of example1.com.
2. linux gateway server will get the request.
3. iptables matches the ip in ipset "glider" and redirect this request to :1081(glider)
4. glider finds the ip in office rule, and then choose a forwarder in office.rule to complete the request.

View File

@ -9,8 +9,5 @@ listen=redir://:1081
dns=:53
dnsserver=8.8.8.8:53
# as a ipset manager
ipset=glider
# parse all *.rule files in rules.d folder
rules-dir=rules.d

View File

@ -1,18 +1,21 @@
forward=http://forwarder4:8080
forward=http://forwarder1:8080
# first connect forwarder1 then forwarder2 then internet
forward=http://forwarder5:8080,socks5://forwarder3:1080
forward=http://forwarder1:8080,socks5://forwarder2:1080
# Round Robin mode: rr
# High Availability mode: ha
strategy=rr
checkwebsite=www.apple.com
checkduration=30
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkinterval=30
# as a ipset manager
ipset=glider
# matches 192.168.0.0/16
cidr=192.168.0.0/16

View File

@ -3,5 +3,5 @@ domain=mycompany.com
domain=mycompany1.com
ip=4.4.4.4
ip=5.5.5.5
cidr=cidr=172.16.101.0/24
cidr=cidr=172.16.102.0/24
cidr=172.16.101.0/24
cidr=172.16.102.0/24

View File

@ -9,14 +9,19 @@ forward=http://forwarder2:8080,socks5://forwarder3:1080
# Round Robin mode: rr
# High Availability mode: ha
strategy=rr
checkwebsite=www.apple.com
checkduration=30
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkinterval=30
# specify a different dns server(if need)
dnsserver=208.67.222.222:53
# as a ipset manager
ipset=glider
# specify destinations
#include=office.list
include=office.list
domain=example1.com
domain=example2.com
# matches ip

View File

@ -32,54 +32,131 @@ verbose=True
# different protocols.
# listen on 8443, serve as http/socks5 proxy on the same port.
listen=:8443
# listen=:8443
listen=127.0.0.1:8443
# listen on 8448 as a ss server.
# listen=ss://AEAD_CHACHA20_POLY1305:pass@:8448
# listen on 8080 as a http proxy server.
listen=http://:8080
# listen=http://:8080
# listen on 1080 as a socks5 proxy server.
listen=socks5://:1080
# listen=socks5://:1080
# listen on 1234 as vless proxy server.
# listen=vless://uuid@:1234
# listen on 1234 as vless proxy server, fallback to 127.0.0.1:8080 http server when client auth failed.
# listen=vless://uuid@:1234?fallback=127.0.0.1:8080
# listen on 1081 as a linux transparent proxy server.
# listen=redir://:1081
# listen on 1082 as a tcp tunnel, all requests to :1082 will be forward to 1.1.1.1:80
# listen=tcptun://:1082=1.1.1.1:80
# listen on 1082 as a linux transparent proxy server(tproxy).
# listen=tproxy://:1082
# listen on 1083 as a udp tunnel, all requests to :1083 will be forward to 1.1.1.1:53
# listen=udptun://:1083=1.1.1.1:53
# http over tls (HTTPS proxy)
# listen=tls://:443?cert=crtFilePath&key=keyFilePath,http://
# listen on 1084 as a udp over tcp tunnel, all requests to :1084 will be forward to 1.1.1.1:53
# listen=uottun://:1084=1.1.1.1:53
# ss over tls
# listen=tls://:443?cert=crtFilePath&key=keyFilePath,ss://AEAD_CHACHA20_POLY1305:pass@
# a dnstun is a special dns forwarder server with a fixed remote dns
# listen on udp port 5353, act as a local dns server,
# forward all requests to 8.8.8.8:53 via tcp protocol
# listen=dnstun://:5353=8.8.8.8:53
# socks5 over unix domain socket
# listen=unix:///dev/shm/socket,socks5://
# socks5 over vm socket
# listen=vsock://:1234,socks5://
# socks5 over kcp
# listen=kcp://aes:key@127.0.0.1:8444?dataShards=10&parityShards=3&mode=fast,socks5://
# vless server
# listen=vless://UUID@:1234
# vless over tls server
# listen=tls://:1234?cert=/path/to/cert&key=/path/to/key,vless://UUID@?fallback=127.0.0.1:80
# vless over ws
# listen=ws://:1234/path?host=domain.com,vless://707f20ea-d4b8-4d1d-8e2e-2c86cb2ed97a@?fallback=127.0.0.1:80
# trojan server
# listen=trojan://PASSWORD@:1234?cert=/path/to/cert&key=/path/to/key&fallback=127.0.0.1
# trojanc server (trojan without tls)
# listen=trojanc://PASSWORD@:1234?fallback=127.0.0.1
# FORWARDERS
# ----------
# Forwarders, we can setup multiple forwarders.
# forward=SCHEME#OPTIONS
# FORWARDER OPTIONS
# priority: set the priority of that forwarder, default:0
# interface: set local interface or ip address used to connect remote server
# Socks5 proxy as forwarder
# forward=socks5://192.168.1.10:1080
# Socks5 proxy as forwarder with priority 100
# forward=socks5://192.168.1.10:1080#priority=100
# Socks5 proxy as forwarder with priority 100 and use `eth0` as source interface
# forward=socks5://192.168.1.10:1080#priority=100&interface=eth0
# Socks5 proxy as forwarder with priority 100 and use `192.168.1.100` as source ip
# forward=socks5://192.168.1.10:1080#priority=100&interface=192.168.1.100
# SS proxy as forwarder
# forward=ss://method:pass@1.1.1.1:8443
# SSR proxy as forwarder
# forward=ssr://method:pass@1.1.1.1:8443?protocol=auth_aes128_md5&protocol_param=xxx&obfs=tls1.2_ticket_auth&obfs_param=yyy
# ssh forwarder
# forward=ssh://user[:pass]@host:port[?key=keypath&timeout=SECONDS]
# forward=ssh://root:pass@host:port
# forward=ssh://root@host:port?key=/path/to/keyfile
# forward=ssh://root@host:port?key=/path/to/keyfile&timeout=5
# http proxy as forwarder
# forward=http://1.1.1.1:8080
# vmess proxy as forwarder
# forward=tls://1.1.1.1:443,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
# trojan as forwarder
# forward=trojan://PASSWORD@1.1.1.1:8080[?serverName=SERVERNAME][&skipVerify=true]
# trojanc as forwarder
# forward=trojanc://PASSWORD@1.1.1.1:8080
# vless forwarder
# forward=vless://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443
# vmess with aead auth
# forward=vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443
# vmess with md5 auth (by setting alterID)
# forward=vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443?alterID=2
# vmess over tls
# forward=tls://server.com:443,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98
# vmess over websocket
# forward=ws://1.1.1.1:80/path?host=server.com,vmess://chacha20-poly1305:5a146038-0b56-4e95-b1dc-5c6f5a32cd98@
# vmess over ws over tls
# forward=tls://server.com:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98
# forward=tls://server.com:443,ws://@/path,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98
# ss over tls
# forward=tls://server.com:443,ss://AEAD_CHACHA20_POLY1305:pass@
# ss over kcp
# forward=kcp://aes:key@127.0.0.1:8444?dataShards=10&parityShards=3&mode=fast,ss://AEAD_CHACHA20_POLY1305:pass@
# ss with simple-obfs
# forward=simple-obfs://1.1.1.1:443?type=tls&host=apple.com,ss://AEAD_CHACHA20_POLY1305:pass@
# socks5 over unix domain socket
# forward=unix:///dev/shm/socket,socks5://
# FORWARDER CHAIN
# ---------------
@ -94,49 +171,127 @@ listen=socks5://:1080
# Round Robin mode: rr
# High Availability mode: ha
# Latency based High Availability mode: lha
# Destination Hashing mode: dh
strategy=rr
# FORWARDER SETTINGS
# ------------------
# We can set some parameters for forwarders.
# forwarder will be set to disabled on how many failures counted(both dial and relay).
maxfailures=3
# timeout for create a connection(seconds)
# dialtimeout=3
# timeout for relay data from proxy server and client(seconds)
# DO NOT change it if you don't know what will happen.
# relaytimeout=0
# FORWARDERS CHECK
# ----------------
# We can check whether a forwarder is available.
# Used to connect via forwarders, if the host is unreachable, the forwarder
# will be set to disabled.
# MUST be a HTTP website server address, format: HOST[:PORT]. HTTPS NOT SUPPORTED.
checkwebsite=www.apple.com
# Forwarder health check:
# check=tcp[://HOST:PORT]: tcp port connect check
# check=http://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
# check=https://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
# e.g. check=https://www.netflix.com/title/81215567#expect=301|404
# check=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, environment variables: FORWARDER_ADDR,FORWARDER_URL
# check=disable: disable health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
# check duration(seconds)
checkduration=30
# check interval(seconds)
checkinterval=30
# timeout to set a forwarder to be disabled(seconds)
checktimeout=10
# switch forwarder only when new_latency < old_latency - tolerance, used in lha mode
checktolerance=100
# use the average latency of the latest N checks
checklatencysamples=10
# check disabled fowarders only
checkdisabledonly=false
# DNS FORWARDING SERVER
# ----------------
# A dns forwarding server listens on UDP and forward dns requests to remote dns server in TCP via forwarders
# we can specify different upstream dns server in rule file for different destinations
# we can specify different upstream dns server in rule file for different destinations.
# Setup a dns forwarding server
dns=:53
# dns=:53
# global remote dns server (you can specify different dns server in rule file)
dnsserver=8.8.8.8:53
dnsserver=1.1.1.1:53
# By default, when glider received udp dns request and there's no forwarder specified,
# it will use udp to query upstream dns servers, otherwise, use tcp;
# you can set dnsalwaystcp=true to always use tcp no matter there is a forwarder or not.
# dnsalwaystcp=false
# IPSET MANAGEMENT
# ----------------
# Create and mange ipset on linux based on destinations in rule files
# - add ip/cidrs in rule files on startup
# - add resolved ips for domains in rule files by dns forwarding server
# Usually used in transparent proxy mode on linux
ipset=glider
# timeout value used in multiple dnsservers switch(seconds)
dnstimeout=3
# maximum TTL value for entries in the CACHE(seconds)
dnsmaxttl=1800
# minimum TTL value for entries in the CACHE(seconds)
dnsminttl=0
# size of CACHE
dnscachesize=4096
# show query log of dns cache
dnscachelog=True
# disable AAAA queries
# dnsnoaaaa=True
# custom records
dnsrecord=www.example.com/1.2.3.4
dnsrecord=www.example.com/2606:2800:220:1:248:1893:25c8:1946
# SERVICES
# service=dhcpd,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
# service=dhcpd-failover,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
# e.g.:
# service=dhcpd,eth1,192.168.1.100,192.168.1.199,720
# service=dhcpd,eth2,192.168.2.100,192.168.2.199,720,fc:23:34:9e:25:01=192.168.2.101,fc:23:34:9e:25:02=192.168.2.102
# INTERFACE SPECIFIC
# ------------------
# Specify global outbound ip/interface.
#
# interface=""
# interface="192.168.1.100"
# interface="eth0"
#
# Specify interface for a forwarder:
# forward=socks5://192.168.1.10:1080#priority=100&interface=eth0
# forward=socks5://192.168.1.10:1080#priority=100&interface=192.168.1.100
# RULE FILES
# ----------
# Specify additional forward rules
# Specify additional forward rules.
#
# specify rules folder, so all *.rule files under this folder will be parsed as rule file
rules-dir=rules.d
# rules-dir=rules.d
#
# specify a rule file
#rulefile=office.rule
#rulefile=home.rule
# INCLUDE CONFIG FILES
# ----------
#include=dnsrecord.inc.conf
#include=more.conf
# ENVIRONMENT VARIABLES
# ----------
# use {$ENV_VAR_NAME} in VALUE to get the Environment Variable value.
# forward=socks5://{$USER_NAME}:{$USER_PASS}@:1080

View File

@ -14,15 +14,20 @@ forward=http://192.168.2.1:8080,socks5://192.168.2.2:1080
strategy=rr
# FORWARDER CHECK SETTINGS
checkwebsite=www.apple.com
checkduration=30
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkinterval=30
# DNS SERVER for domains in this rule file
dnsserver=208.67.222.222:53
# IPSET
# specify a ipset for destinations in this rule file
#ipset=office
# IPSET MANAGEMENT
# ----------------
# Create and mange ipset on linux based on destinations in rule files
# - add ip/cidrs in rule files on startup
# - add resolved ips for domains in rule files by dns forwarding server
# Usually used in transparent proxy mode on linux
# Note: this will create 2 ipsets, glider for ipv4 and glider6 for ipv6
ipset=glider
# DESTINATIONS
# ------------
@ -30,7 +35,7 @@ dnsserver=208.67.222.222:53
# INCLUDE FILE
# we can include a list file with only destinations settings
include=office.list.example
include=office.list
# matches example.com and *.example.com
domain=example.com

View File

@ -0,0 +1,7 @@
forward=reject://
ipset=glider
domain=pornhub.com
domain=amazon.com

127
dns/cache.go Normal file
View File

@ -0,0 +1,127 @@
package dns
import (
"sync"
"time"
)
// LruCache is the struct of LruCache.
type LruCache struct {
mu sync.Mutex
size int
head *item
tail *item
cache map[string]*item
store map[string][]byte
}
// item is the struct of cache item.
type item struct {
key string
val []byte
exp int64
prev *item
next *item
}
// NewLruCache returns a new LruCache.
func NewLruCache(size int) *LruCache {
// init 2 items here, it doesn't matter cuz they will be deleted when the cache is full
head, tail := &item{key: "head"}, &item{key: "tail"}
head.next, tail.prev = tail, head
c := &LruCache{
size: size,
head: head,
tail: tail,
cache: make(map[string]*item, size),
store: make(map[string][]byte),
}
c.cache[head.key], c.cache[tail.key] = head, tail
return c
}
// Get gets an item from cache.
func (c *LruCache) Get(k string) (v []byte, expired bool) {
c.mu.Lock()
defer c.mu.Unlock()
if v, ok := c.store[k]; ok {
return v, false
}
if it, ok := c.cache[k]; ok {
v = it.val
if it.exp < time.Now().Unix() {
expired = true
}
c.moveToHead(it)
}
return
}
// Set sets an item with key, value, and ttl(seconds).
// if the ttl is zero, this item will be set and never be deleted.
// if the key exists, update it with value and exp and move it to head.
// if the key does not exist, put a new item to the cache's head.
// finally, remove the tail if the cache is full.
func (c *LruCache) Set(k string, v []byte, ttl int) {
c.mu.Lock()
defer c.mu.Unlock()
if ttl == 0 {
c.store[k] = v
return
}
exp := time.Now().Add(time.Second * time.Duration(ttl)).Unix()
if it, ok := c.cache[k]; ok {
it.val = v
it.exp = exp
c.moveToHead(it)
return
}
c.putToHead(k, v, exp)
// NOTE: the cache size will always >= 2,
// but it doesn't matter in our environment.
if len(c.cache) > c.size {
c.removeTail()
}
}
// putToHead puts a new item to cache's head.
func (c *LruCache) putToHead(k string, v []byte, exp int64) {
it := &item{key: k, val: v, exp: exp, prev: nil, next: c.head}
it.prev = nil
it.next = c.head
c.head.prev = it
c.head = it
c.cache[k] = it
}
// moveToHead moves an existing item to cache's head.
func (c *LruCache) moveToHead(it *item) {
if it != c.head {
if c.tail == it {
c.tail = it.prev
c.tail.next = nil
} else {
it.prev.next = it.next
it.next.prev = it.prev
}
it.prev = nil
it.next = c.head
c.head.prev = it
c.head = it
}
}
// removeTail removes the tail from cache.
func (c *LruCache) removeTail() {
delete(c.cache, c.tail.key)
c.tail.prev.next = nil
c.tail = c.tail.prev
}

344
dns/client.go Normal file
View File

@ -0,0 +1,344 @@
package dns
import (
"encoding/binary"
"errors"
"io"
"net"
"net/netip"
"strconv"
"strings"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/proxy"
)
// AnswerHandler function handles the dns TypeA or TypeAAAA answer.
type AnswerHandler func(domain string, ip netip.Addr) error
// Config for dns.
type Config struct {
Servers []string
Timeout int
MaxTTL int
MinTTL int
Records []string
AlwaysTCP bool
CacheSize int
CacheLog bool
NoAAAA bool
}
// Client is a dns client struct.
type Client struct {
proxy proxy.Proxy
cache *LruCache
config *Config
upStream *UPStream
upStreamMap map[string]*UPStream
handlers []AnswerHandler
}
// NewClient returns a new dns client.
func NewClient(proxy proxy.Proxy, config *Config) (*Client, error) {
c := &Client{
proxy: proxy,
cache: NewLruCache(config.CacheSize),
config: config,
upStream: NewUPStream(config.Servers),
upStreamMap: make(map[string]*UPStream),
}
// custom records
for _, record := range config.Records {
if err := c.AddRecord(record); err != nil {
log.F("[dns] add record '%s' error: %s", record, err)
}
}
return c, nil
}
// Exchange handles request message and returns response message.
// TODO: optimize it
func (c *Client) Exchange(reqBytes []byte, clientAddr string, preferTCP bool) ([]byte, error) {
req, err := UnmarshalMessage(reqBytes)
if err != nil {
return nil, err
}
if c.config.NoAAAA && req.Question.QTYPE == QTypeAAAA {
respBytes := valCopy(reqBytes)
respBytes[2] |= uint8(ResponseMsg) << 7
return respBytes, nil
}
if req.Question.QTYPE == QTypeA || req.Question.QTYPE == QTypeAAAA {
if v, expired := c.cache.Get(qKey(req.Question)); len(v) > 2 {
v = valCopy(v)
binary.BigEndian.PutUint16(v[:2], req.ID)
if c.config.CacheLog {
log.F("[dns] %s <-> cache, type: %d, %s",
clientAddr, req.Question.QTYPE, req.Question.QNAME)
}
if expired { // update cache
go func(qname string, reqBytes []byte, preferTCP bool) {
defer pool.PutBuffer(reqBytes)
if dnsServer, network, dialerAddr, respBytes, err := c.exchange(qname, reqBytes, preferTCP); err == nil {
c.handleAnswer(respBytes, "cache", dnsServer, network, dialerAddr)
}
}(req.Question.QNAME, valCopy(reqBytes), preferTCP)
}
return v, nil
}
}
dnsServer, network, dialerAddr, respBytes, err := c.exchange(req.Question.QNAME, reqBytes, preferTCP)
if err != nil {
return nil, err
}
if req.Question.QTYPE != QTypeA && req.Question.QTYPE != QTypeAAAA {
log.F("[dns] %s <-> %s(%s) via %s, type: %d, %s",
clientAddr, dnsServer, network, dialerAddr, req.Question.QTYPE, req.Question.QNAME)
return respBytes, nil
}
err = c.handleAnswer(respBytes, clientAddr, dnsServer, network, dialerAddr)
return respBytes, err
}
func (c *Client) handleAnswer(respBytes []byte, clientAddr, dnsServer, network, dialerAddr string) error {
resp, err := UnmarshalMessage(respBytes)
if err != nil {
return err
}
ips, ttl := c.extractAnswer(resp)
if ttl > c.config.MaxTTL {
ttl = c.config.MaxTTL
} else if ttl < c.config.MinTTL {
ttl = c.config.MinTTL
}
if ttl <= 0 { // we got a null result
ttl = 1800
}
c.cache.Set(qKey(resp.Question), valCopy(respBytes), ttl)
log.F("[dns] %s <-> %s(%s) via %s, %s/%d: %s, ttl: %ds",
clientAddr, dnsServer, network, dialerAddr, resp.Question.QNAME, resp.Question.QTYPE, strings.Join(ips, ","), ttl)
return nil
}
func (c *Client) extractAnswer(resp *Message) ([]string, int) {
var ips []string
ttl := c.config.MinTTL
for _, answer := range resp.Answers {
if answer.TYPE == QTypeA || answer.TYPE == QTypeAAAA {
if answer.IP.IsValid() && !answer.IP.IsUnspecified() {
for _, h := range c.handlers {
h(resp.Question.QNAME, answer.IP)
}
ips = append(ips, answer.IP.String())
}
if answer.TTL != 0 {
ttl = int(answer.TTL)
}
}
}
return ips, ttl
}
// exchange choose a upstream dns server based on qname, communicate with it on the network.
func (c *Client) exchange(qname string, reqBytes []byte, preferTCP bool) (
server, network, dialerAddr string, respBytes []byte, err error) {
// use tcp to connect upstream server default
network = "tcp"
dialer := c.proxy.NextDialer(qname + ":0")
// if we are resolving a domain which uses a forwarder `REJECT`, then use `DIRECT` instead
// so we can resolve it correctly.
// TODO: dialer.Addr() == "REJECT", tricky
if dialer.Addr() == "REJECT" {
dialer = c.proxy.NextDialer("direct:0")
}
// If client uses udp and no forwarders specified, use udp
// TODO: dialer.Addr() == "DIRECT", tricky
if !preferTCP && !c.config.AlwaysTCP && dialer.Addr() == "DIRECT" {
network = "udp"
}
ups := c.UpStream(qname)
server = ups.Server()
for range ups.Len() {
var rc net.Conn
rc, err = dialer.Dial(network, server)
if err != nil {
newServer := ups.SwitchIf(server)
log.F("[dns] error in resolving %s, failed to connect to server %v via %s: %v, next server: %s",
qname, server, dialer.Addr(), err, newServer)
server = newServer
continue
}
defer rc.Close()
// TODO: support timeout setting for different upstream server
if c.config.Timeout > 0 {
rc.SetDeadline(time.Now().Add(time.Duration(c.config.Timeout) * time.Second))
}
switch network {
case "tcp":
respBytes, err = c.exchangeTCP(rc, reqBytes)
case "udp":
respBytes, err = c.exchangeUDP(rc, reqBytes)
}
if err == nil {
break
}
newServer := ups.SwitchIf(server)
log.F("[dns] error in resolving %s, failed to exchange with server %v via %s: %v, next server: %s",
qname, server, dialer.Addr(), err, newServer)
server = newServer
}
// if all dns upstreams failed, then maybe the forwarder is not available.
if err != nil {
c.proxy.Record(dialer, false)
}
return server, network, dialer.Addr(), respBytes, err
}
// exchangeTCP exchange with server over tcp.
func (c *Client) exchangeTCP(rc net.Conn, reqBytes []byte) ([]byte, error) {
lenBuf := pool.GetBuffer(2)
defer pool.PutBuffer(lenBuf)
binary.BigEndian.PutUint16(lenBuf, uint16(len(reqBytes)))
if _, err := (&net.Buffers{lenBuf, reqBytes}).WriteTo(rc); err != nil {
return nil, err
}
var respLen uint16
if err := binary.Read(rc, binary.BigEndian, &respLen); err != nil {
return nil, err
}
respBytes := pool.GetBuffer(int(respLen))
_, err := io.ReadFull(rc, respBytes)
if err != nil {
return nil, err
}
return respBytes, nil
}
// exchangeUDP exchange with server over udp.
func (c *Client) exchangeUDP(rc net.Conn, reqBytes []byte) ([]byte, error) {
if _, err := rc.Write(reqBytes); err != nil {
return nil, err
}
respBytes := pool.GetBuffer(UDPMaxLen)
n, err := rc.Read(respBytes)
if err != nil {
return nil, err
}
return respBytes[:n], nil
}
// SetServers sets upstream dns servers for the given domain.
func (c *Client) SetServers(domain string, servers []string) {
c.upStreamMap[strings.ToLower(domain)] = NewUPStream(servers)
}
// UpStream returns upstream dns server for the given domain.
func (c *Client) UpStream(domain string) *UPStream {
domain = strings.ToLower(domain)
for i := len(domain); i != -1; {
i = strings.LastIndexByte(domain[:i], '.')
if upstream, ok := c.upStreamMap[domain[i+1:]]; ok {
return upstream
}
}
return c.upStream
}
// AddHandler adds a custom handler to handle the resolved result (A and AAAA).
func (c *Client) AddHandler(h AnswerHandler) {
c.handlers = append(c.handlers, h)
}
// AddRecord adds custom record to dns cache, format:
// www.example.com/1.2.3.4 or www.example.com/2606:2800:220:1:248:1893:25c8:1946
func (c *Client) AddRecord(record string) error {
domain, ip, found := strings.Cut(record, "/")
if !found {
return errors.New("wrong record format, must contain '/'")
}
m, err := MakeResponse(domain, ip, uint32(c.config.MaxTTL))
if err != nil {
log.F("[dns] add custom record error: %s", err)
return err
}
wb := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(wb)
_, err = m.MarshalTo(wb)
if err != nil {
return err
}
c.cache.Set(qKey(m.Question), valCopy(wb.Bytes()), 0)
return nil
}
// MakeResponse makes a dns response message for the given domain and ip address.
// Note: you should make sure ttl > 0.
func MakeResponse(domain, ip string, ttl uint32) (*Message, error) {
addr, err := netip.ParseAddr(ip)
if err != nil {
return nil, err
}
var qtype, rdlen uint16 = QTypeA, net.IPv4len
if addr.Is6() {
qtype, rdlen = QTypeAAAA, net.IPv6len
}
m := NewMessage(0, ResponseMsg)
m.SetQuestion(NewQuestion(qtype, domain))
rr := &RR{NAME: domain, TYPE: qtype, CLASS: ClassINET,
TTL: ttl, RDLENGTH: rdlen, RDATA: addr.AsSlice()}
m.AddAnswer(rr)
return m, nil
}
func qKey(q *Question) string {
return q.QNAME + "/" + strconv.FormatUint(uint64(q.QTYPE), 10)
}
func valCopy(v []byte) (b []byte) {
if v != nil {
b = pool.GetBuffer(len(v))
copy(b, v)
}
return
}

View File

@ -1,470 +0,0 @@
// https://tools.ietf.org/html/rfc1035
package dns
import (
"encoding/binary"
"errors"
"io"
"net"
"strings"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/proxy"
)
// HeaderLen is the length of dns msg header
const HeaderLen = 12
// UDPMaxLen is the max size of udp dns request.
// https://tools.ietf.org/html/rfc1035#section-4.2.1
// Messages carried by UDP are restricted to 512 bytes (not counting the IP
// or UDP headers). Longer messages are truncated and the TC bit is set in
// the header.
// TODO: If the request length > 512 then the client will send TCP packets instead,
// so we should also serve tcp requests.
const UDPMaxLen = 512
// QType .
const (
QTypeA = 1 //ipv4
QTypeAAAA = 28 ///ipv6
)
// Msg format
// https://tools.ietf.org/html/rfc1035#section-4.1
// All communications inside of the domain protocol are carried in a single
// format called a message. The top level format of message is divided
// into 5 sections (some of which are empty in certain cases) shown below:
//
// +---------------------+
// | Header |
// +---------------------+
// | Question | the question for the name server
// +---------------------+
// | Answer | RRs answering the question
// +---------------------+
// | Authority | RRs pointing toward an authority
// +---------------------+
// | Additional | RRs holding additional information
// type Msg struct {
// Header
// Questions []Question
// Answers []RR
// }
// Header format
// https://tools.ietf.org/html/rfc1035#section-4.1.1
// The header contains the following fields:
//
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ID |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | QDCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ANCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | NSCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ARCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
//
// type Header struct {
// ID uint16
// }
// Question format
// https://tools.ietf.org/html/rfc1035#section-4.1.2
// The question section is used to carry the "question" in most queries,
// i.e., the parameters that define what is being asked. The section
// contains QDCOUNT (usually 1) entries, each of the following format:
//
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | |
// / QNAME /
// / /
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | QTYPE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | QCLASS |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
type Question struct {
QNAME string
QTYPE uint16
QCLASS uint16
Offset int
}
// RR format
// https://tools.ietf.org/html/rfc1035#section-3.2.1
// All RRs have the same top level format shown below:
//
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | |
// / /
// / NAME /
// | |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | TYPE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | CLASS |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | TTL |
// | |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | RDLENGTH |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
// / RDATA /
// / /
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
type RR struct {
// NAME string
TYPE uint16
CLASS uint16
TTL uint32
RDLENGTH uint16
RDATA []byte
IP string
}
// AnswerHandler function handles the dns TypeA or TypeAAAA answer
type AnswerHandler func(Domain, ip string) error
// DNS .
type DNS struct {
dialer proxy.Dialer
addr string
Tunnel bool
DNSServer string
DNSServerMap map[string]string
AnswerHandlers []AnswerHandler
}
// NewDNS returns a dns forwarder. client[dns.udp] -> glider[tcp] -> forwarder[dns.tcp] -> remote dns addr
func NewDNS(addr, raddr string, dialer proxy.Dialer, tunnel bool) (*DNS, error) {
s := &DNS{
dialer: dialer,
addr: addr,
Tunnel: tunnel,
DNSServer: raddr,
DNSServerMap: make(map[string]string),
}
return s, nil
}
// ListenAndServe .
func (s *DNS) ListenAndServe() {
go s.ListenAndServeTCP()
s.ListenAndServeUDP()
}
// ListenAndServeUDP .
func (s *DNS) ListenAndServeUDP() {
c, err := net.ListenPacket("udp", s.addr)
if err != nil {
log.F("[dns] failed to listen on %s, error: %v", s.addr, err)
return
}
defer c.Close()
log.F("[dns] listening UDP on %s", s.addr)
for {
b := make([]byte, UDPMaxLen)
n, clientAddr, err := c.ReadFrom(b)
if err != nil {
log.F("[dns] local read error: %v", err)
continue
}
reqLen := uint16(n)
// TODO: check here
if reqLen <= HeaderLen+2 {
log.F("[dns] not enough data")
continue
}
reqMsg := b[:n]
go func() {
_, respMsg, err := s.Exchange(reqLen, reqMsg, clientAddr.String())
if err != nil {
log.F("[dns] error in exchange: %s", err)
return
}
_, err = c.WriteTo(respMsg, clientAddr)
if err != nil {
log.F("[dns] error in local write: %s", err)
return
}
}()
}
}
// ListenAndServeTCP .
func (s *DNS) ListenAndServeTCP() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.F("[dns]-tcp error: %v", err)
return
}
log.F("[dns]-tcp listening TCP on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[dns]-tcp error: failed to accept: %v", err)
continue
}
go s.ServeTCP(c)
}
}
// ServeTCP .
func (s *DNS) ServeTCP(c net.Conn) {
defer c.Close()
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
var reqLen uint16
if err := binary.Read(c, binary.BigEndian, &reqLen); err != nil {
log.F("[dns]-tcp failed to get request length: %v", err)
return
}
// TODO: check here
if reqLen <= HeaderLen+2 {
log.F("[dns]-tcp not enough data")
return
}
reqMsg := make([]byte, reqLen)
_, err := io.ReadFull(c, reqMsg)
if err != nil {
log.F("[dns]-tcp error in read reqMsg %s", err)
return
}
respLen, respMsg, err := s.Exchange(reqLen, reqMsg, c.RemoteAddr().String())
if err != nil {
log.F("[dns]-tcp error in exchange: %s", err)
return
}
if err := binary.Write(c, binary.BigEndian, respLen); err != nil {
log.F("[dns]-tcp error in local write respLen: %s", err)
return
}
if err := binary.Write(c, binary.BigEndian, respMsg); err != nil {
log.F("[dns]-tcp error in local write respMsg: %s", err)
return
}
}
// Exchange handles request msg and returns response msg
// TODO: multiple questions support, parse header to get the number of questions
func (s *DNS) Exchange(reqLen uint16, reqMsg []byte, addr string) (respLen uint16, respMsg []byte, err error) {
// fmt.Printf("\ndns req len %d:\n%s\n", reqLen, hex.Dump(reqMsg[:]))
query, err := parseQuestion(reqMsg)
if err != nil {
log.F("[dns] error in parseQuestion reqMsg: %s", err)
return
}
dnsServer := s.GetServer(query.QNAME)
rc, err := s.dialer.NextDialer(query.QNAME+":53").Dial("tcp", dnsServer)
if err != nil {
log.F("[dns] failed to connect to server %v: %v", dnsServer, err)
return
}
defer rc.Close()
if err = binary.Write(rc, binary.BigEndian, reqLen); err != nil {
log.F("[dns] failed to write req length: %v", err)
return
}
if err = binary.Write(rc, binary.BigEndian, reqMsg); err != nil {
log.F("[dns] failed to write req message: %v", err)
return
}
if err = binary.Read(rc, binary.BigEndian, &respLen); err != nil {
log.F("[dns] failed to read response length: %v", err)
return
}
respMsg = make([]byte, respLen)
_, err = io.ReadFull(rc, respMsg)
if err != nil {
log.F("[dns] error in read respMsg %s\n", err)
return
}
// fmt.Printf("\ndns resp len %d:\n%s\n", respLen, hex.Dump(respMsg[:]))
var ip string
respReq, err := parseQuestion(respMsg)
if err != nil {
log.F("[dns] error in parseQuestion respMsg: %s", err)
return
}
if (respReq.QTYPE == QTypeA || respReq.QTYPE == QTypeAAAA) &&
len(respMsg) > respReq.Offset {
var answers []*RR
answers, err = parseAnswers(respMsg[respReq.Offset:])
if err != nil {
log.F("[dns] error in parseAnswers: %s", err)
return
}
for _, answer := range answers {
for _, h := range s.AnswerHandlers {
h(respReq.QNAME, answer.IP)
}
if answer.IP != "" {
ip += answer.IP + ","
}
}
}
log.F("[dns] %s <-> %s, type: %d, %s: %s", addr, dnsServer, query.QTYPE, query.QNAME, ip)
return
}
// SetServer .
func (s *DNS) SetServer(domain, server string) {
s.DNSServerMap[domain] = server
}
// GetServer .
func (s *DNS) GetServer(domain string) string {
if !s.Tunnel {
domainParts := strings.Split(domain, ".")
length := len(domainParts)
for i := length - 2; i >= 0; i-- {
domain := strings.Join(domainParts[i:length], ".")
if server, ok := s.DNSServerMap[domain]; ok {
return server
}
}
}
return s.DNSServer
}
// AddAnswerHandler .
func (s *DNS) AddAnswerHandler(h AnswerHandler) {
s.AnswerHandlers = append(s.AnswerHandlers, h)
}
func parseQuestion(p []byte) (*Question, error) {
q := &Question{}
lenP := len(p)
var i int
var domain []byte
for i = HeaderLen; i < lenP; {
l := int(p[i])
if l == 0 {
i++
break
}
if lenP <= i+l+1 {
return nil, errors.New("not enough data for QNAME")
}
domain = append(domain, p[i+1:i+l+1]...)
domain = append(domain, '.')
i = i + l + 1
}
if len(domain) == 0 {
return nil, errors.New("no QNAME")
}
q.QNAME = string(domain[:len(domain)-1])
if lenP < i+4 {
return nil, errors.New("not enough data")
}
q.QTYPE = binary.BigEndian.Uint16(p[i:])
q.QCLASS = binary.BigEndian.Uint16(p[i+2:])
q.Offset = i + 4
return q, nil
}
func parseAnswers(p []byte) ([]*RR, error) {
var answers []*RR
lenP := len(p)
for i := 0; i < lenP; {
// https://tools.ietf.org/html/rfc1035#section-4.1.4
// "Message compression",
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | 1 1| OFFSET |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
if p[i]>>6 == 3 {
i += 2
} else {
// TODO: none compressed query name and Additional records will be ignored
break
}
if lenP <= i+10 {
return nil, errors.New("not enough data")
}
answer := &RR{}
answer.TYPE = binary.BigEndian.Uint16(p[i:])
answer.CLASS = binary.BigEndian.Uint16(p[i+2:])
answer.TTL = binary.BigEndian.Uint32(p[i+4:])
answer.RDLENGTH = binary.BigEndian.Uint16(p[i+8:])
answer.RDATA = p[i+10 : i+10+int(answer.RDLENGTH)]
if answer.TYPE == QTypeA {
answer.IP = net.IP(answer.RDATA[:net.IPv4len]).String()
} else if answer.TYPE == QTypeAAAA {
answer.IP = net.IP(answer.RDATA[:net.IPv6len]).String()
}
answers = append(answers, answer)
i = i + 10 + int(answer.RDLENGTH)
}
return answers, nil
}

527
dns/message.go Normal file
View File

@ -0,0 +1,527 @@
package dns
import (
"bytes"
"encoding/binary"
"errors"
"io"
"math/rand/v2"
"net/netip"
"strings"
)
// UDPMaxLen is the max size of udp dns request.
// https://www.dnsflagday.net/2020/
const UDPMaxLen = 1232
// HeaderLen is the length of dns msg header.
const HeaderLen = 12
// MsgType is the dns Message type.
type MsgType byte
// Message types.
const (
QueryMsg MsgType = 0
ResponseMsg MsgType = 1
)
// Query types.
const (
QTypeA uint16 = 1 //ipv4
QTypeAAAA uint16 = 28 ///ipv6
)
// ClassINET .
const ClassINET uint16 = 1
// Message format:
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1
// All communications inside of the domain protocol are carried in a single
// format called a message. The top level format of message is divided
// into 5 sections (some of which are empty in certain cases) shown below:
//
// +---------------------+
// | Header |
// +---------------------+
// | Question | the question for the name server
// +---------------------+
// | Answer | RRs answering the question
// +---------------------+
// | Authority | RRs pointing toward an authority
// +---------------------+
// | Additional | RRs holding additional information
type Message struct {
Header
// most dns implementation only support 1 question
Question *Question
Answers []*RR
Authority []*RR
Additional []*RR
// used in UnmarshalMessage
unMarshaled []byte
}
// NewMessage returns a new message.
func NewMessage(id uint16, msgType MsgType) *Message {
if id == 0 {
id = uint16(rand.Uint32())
}
m := &Message{Header: Header{ID: id}}
m.SetMsgType(msgType)
return m
}
// SetQuestion sets a question to dns message.
func (m *Message) SetQuestion(q *Question) error {
m.Question = q
m.Header.SetQdcount(1)
return nil
}
// AddAnswer adds an answer to dns message.
func (m *Message) AddAnswer(rr *RR) error {
m.Answers = append(m.Answers, rr)
return nil
}
// Marshal marshals message struct to []byte.
func (m *Message) Marshal() ([]byte, error) {
buf := &bytes.Buffer{}
if _, err := m.MarshalTo(buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// MarshalTo marshals message struct to []byte and write to w.
func (m *Message) MarshalTo(w io.Writer) (n int, err error) {
m.Header.SetQdcount(1)
m.Header.SetAncount(len(m.Answers))
nn := 0
nn, err = m.Header.MarshalTo(w)
if err != nil {
return
}
n += nn
nn, err = m.Question.MarshalTo(w)
if err != nil {
return
}
n += nn
for _, answer := range m.Answers {
nn, err = answer.MarshalTo(w)
if err != nil {
return
}
n += nn
}
return
}
// UnmarshalMessage unmarshals []bytes to Message.
func UnmarshalMessage(b []byte) (*Message, error) {
if len(b) < HeaderLen {
return nil, errors.New("UnmarshalMessage: not enough data")
}
m := &Message{unMarshaled: b}
if err := UnmarshalHeader(b[:HeaderLen], &m.Header); err != nil {
return nil, err
}
q := &Question{}
qLen, err := m.UnmarshalQuestion(b[HeaderLen:], q)
if err != nil {
return nil, err
}
m.SetQuestion(q)
// resp answers
rrIdx := HeaderLen + qLen
for range int(m.Header.ANCOUNT) {
rr := &RR{}
rrLen, err := m.UnmarshalRR(rrIdx, rr)
if err != nil {
return nil, err
}
m.AddAnswer(rr)
rrIdx += rrLen
}
m.Header.SetAncount(len(m.Answers))
return m, nil
}
// Header format:
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.1
// The header contains the following fields:
//
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ID |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | QDCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ANCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | NSCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ARCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
type Header struct {
ID uint16
Bits uint16
QDCOUNT uint16
ANCOUNT uint16
NSCOUNT uint16
ARCOUNT uint16
}
// SetMsgType sets the message type.
func (h *Header) SetMsgType(qr MsgType) {
h.Bits |= uint16(qr) << 15
}
// SetTC sets the tc flag.
func (h *Header) SetTC(tc int) {
h.Bits |= uint16(tc) << 9
}
// SetQdcount sets query count, most dns servers only support 1 query per request.
func (h *Header) SetQdcount(qdcount int) {
h.QDCOUNT = uint16(qdcount)
}
// SetAncount sets answers count.
func (h *Header) SetAncount(ancount int) {
h.ANCOUNT = uint16(ancount)
}
// Not used now, but keep it for future use.
// func (h *Header) setFlag(QR uint16, Opcode uint16, AA uint16,
// TC uint16, RD uint16, RA uint16, RCODE uint16) {
// h.Bits = QR<<15 + Opcode<<11 + AA<<10 + TC<<9 + RD<<8 + RA<<7 + RCODE
// }
// MarshalTo marshals header struct to []byte and write to w.
func (h *Header) MarshalTo(w io.Writer) (int, error) {
return HeaderLen, binary.Write(w, binary.BigEndian, h)
}
// UnmarshalHeader unmarshals []bytes to Header.
func UnmarshalHeader(b []byte, h *Header) error {
if h == nil {
return errors.New("unmarshal header must not be nil")
}
if len(b) != HeaderLen {
return errors.New("unmarshal header bytes has an unexpected size")
}
h.ID = binary.BigEndian.Uint16(b[:2])
h.Bits = binary.BigEndian.Uint16(b[2:4])
h.QDCOUNT = binary.BigEndian.Uint16(b[4:6])
h.ANCOUNT = binary.BigEndian.Uint16(b[6:8])
h.NSCOUNT = binary.BigEndian.Uint16(b[8:10])
h.ARCOUNT = binary.BigEndian.Uint16(b[10:])
return nil
}
// Question format:
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.2
// The question section is used to carry the "question" in most queries,
// i.e., the parameters that define what is being asked. The section
// contains QDCOUNT (usually 1) entries, each of the following format:
//
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | |
// / QNAME /
// / /
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | QTYPE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | QCLASS |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
type Question struct {
QNAME string
QTYPE uint16
QCLASS uint16
}
// NewQuestion returns a new dns question.
func NewQuestion(qtype uint16, domain string) *Question {
return &Question{
QNAME: domain,
QTYPE: qtype,
QCLASS: ClassINET,
}
}
// MarshalTo marshals Question struct to []byte and write to w.
func (q *Question) MarshalTo(w io.Writer) (n int, err error) {
n, err = MarshalDomainTo(w, q.QNAME)
if err != nil {
return
}
if err = binary.Write(w, binary.BigEndian, q.QTYPE); err != nil {
return
}
n += 2
if err = binary.Write(w, binary.BigEndian, q.QCLASS); err != nil {
return
}
n += 2
return
}
// UnmarshalQuestion unmarshals []bytes to Question.
func (m *Message) UnmarshalQuestion(b []byte, q *Question) (n int, err error) {
if q == nil {
return 0, errors.New("unmarshal question must not be nil")
}
if len(b) <= 5 {
return 0, errors.New("UnmarshalQuestion: not enough data")
}
sb := new(strings.Builder)
sb.Grow(32)
idx, err := m.UnmarshalDomainTo(sb, b)
if err != nil {
return 0, err
}
q.QNAME = sb.String()
q.QTYPE = binary.BigEndian.Uint16(b[idx : idx+2])
q.QCLASS = binary.BigEndian.Uint16(b[idx+2 : idx+4])
return idx + 3 + 1, nil
}
// RR format:
// https://www.rfc-editor.org/rfc/rfc1035#section-3.2.1
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.3
// The answer, authority, and additional sections all share the same
// format: a variable number of resource records, where the number of
// records is specified in the corresponding count field in the header.
// Each resource record has the following format:
//
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | |
// / /
// / NAME /
// | |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | TYPE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | CLASS |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | TTL |
// | |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | RDLENGTH |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
// / RDATA /
// / /
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
type RR struct {
NAME string
TYPE uint16
CLASS uint16
TTL uint32
RDLENGTH uint16
RDATA []byte
IP netip.Addr
}
// NewRR returns a new dns rr.
func NewRR() *RR {
return &RR{}
}
// MarshalTo marshals RR struct to []byte and write to w.
func (rr *RR) MarshalTo(w io.Writer) (n int, err error) {
n, err = MarshalDomainTo(w, rr.NAME)
if err != nil {
return
}
if err = binary.Write(w, binary.BigEndian, rr.TYPE); err != nil {
return
}
n += 2
if err = binary.Write(w, binary.BigEndian, rr.CLASS); err != nil {
return
}
n += 2
if err = binary.Write(w, binary.BigEndian, rr.TTL); err != nil {
return
}
n += 4
err = binary.Write(w, binary.BigEndian, rr.RDLENGTH)
if err != nil {
return
}
n += 2
if _, err = w.Write(rr.RDATA); err != nil {
return
}
n += len(rr.RDATA)
return
}
// UnmarshalRR unmarshals []bytes to RR.
func (m *Message) UnmarshalRR(start int, rr *RR) (n int, err error) {
if rr == nil {
return 0, errors.New("unmarshal rr must not be nil")
}
p := m.unMarshaled[start:]
sb := new(strings.Builder)
sb.Grow(32)
n, err = m.UnmarshalDomainTo(sb, p)
if err != nil {
return 0, err
}
rr.NAME = sb.String()
if len(p) <= n+10 {
return 0, errors.New("UnmarshalRR: not enough data")
}
rr.TYPE = binary.BigEndian.Uint16(p[n:])
rr.CLASS = binary.BigEndian.Uint16(p[n+2:])
rr.TTL = binary.BigEndian.Uint32(p[n+4:])
rr.RDLENGTH = binary.BigEndian.Uint16(p[n+8:])
if len(p) < n+10+int(rr.RDLENGTH) {
return 0, errors.New("UnmarshalRR: not enough data for RDATA")
}
rr.RDATA = p[n+10 : n+10+int(rr.RDLENGTH)]
if rr.TYPE == QTypeA {
rr.IP = netip.AddrFrom4(*(*[4]byte)(rr.RDATA[:4]))
} else if rr.TYPE == QTypeAAAA {
rr.IP = netip.AddrFrom16(*(*[16]byte)(rr.RDATA[:16]))
}
n = n + 10 + int(rr.RDLENGTH)
return n, nil
}
// MarshalDomainTo marshals domain string struct to []byte and write to w.
func MarshalDomainTo(w io.Writer, domain string) (n int, err error) {
nn := 0
for _, seg := range strings.Split(domain, ".") {
nn, err = w.Write([]byte{byte(len(seg))})
if err != nil {
return
}
n += nn
nn, err = io.WriteString(w, seg)
if err != nil {
return
}
n += nn
}
nn, err = w.Write([]byte{0x00})
if err != nil {
return
}
n += nn
return
}
// UnmarshalDomainTo gets domain from bytes to string builder.
func (m *Message) UnmarshalDomainTo(sb *strings.Builder, b []byte) (int, error) {
var idx, size int
for len(b[idx:]) != 0 {
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.4
// "Message compression",
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | 1 1| OFFSET |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
if b[idx]&0xC0 == 0xC0 {
if len(b[idx:]) < 2 {
return 0, errors.New("UnmarshalDomainTo: not enough size for compressed domain")
}
offset := binary.BigEndian.Uint16(b[idx : idx+2])
if err := m.UnmarshalDomainPointTo(sb, int(offset&0x3FFF)); err != nil {
return 0, err
}
idx += 2
break
}
size = int(b[idx])
idx++
// root domain name
if size == 0 {
break
}
if size > 63 {
return 0, errors.New("UnmarshalDomainTo: label size larger than 63")
}
if idx+size > len(b) {
return 0, errors.New("UnmarshalDomainTo: label size larger than msg length")
}
if sb.Len() > 0 {
sb.WriteByte('.')
}
sb.Write(b[idx : idx+size])
idx += size
}
return idx, nil
}
// UnmarshalDomainPointTo gets domain from offset point to string builder.
func (m *Message) UnmarshalDomainPointTo(sb *strings.Builder, offset int) error {
if offset > len(m.unMarshaled) {
return errors.New("UnmarshalDomainPointTo: offset larger than msg length")
}
_, err := m.UnmarshalDomainTo(sb, m.unMarshaled[offset:])
return err
}

151
dns/server.go Normal file
View File

@ -0,0 +1,151 @@
package dns
import (
"encoding/binary"
"io"
"net"
"sync"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/proxy"
)
// conn timeout, in seconds.
const timeout = 30
// Server is a dns server struct.
type Server struct {
addr string
// Client is used to communicate with upstream dns servers
*Client
}
// NewServer returns a new dns server.
func NewServer(addr string, p proxy.Proxy, config *Config) (*Server, error) {
c, err := NewClient(p, config)
if err != nil {
return nil, err
}
s := &Server{
addr: addr,
Client: c,
}
return s, nil
}
// Start starts the dns forwarding server.
// We use WaitGroup here to ensure both udp and tcp serer are completly running,
// so we can start any other services later, since they may rely on dns service.
func (s *Server) Start() {
var wg sync.WaitGroup
wg.Add(2)
go s.ListenAndServeTCP(&wg)
go s.ListenAndServeUDP(&wg)
wg.Wait()
}
// ListenAndServeUDP listen and serves on udp port.
func (s *Server) ListenAndServeUDP(wg *sync.WaitGroup) {
pc, err := net.ListenPacket("udp", s.addr)
wg.Done()
if err != nil {
log.F("[dns] failed to listen on %s, error: %v", s.addr, err)
return
}
defer pc.Close()
log.F("[dns] listening UDP on %s", s.addr)
for {
reqBytes := pool.GetBuffer(UDPMaxLen)
n, caddr, err := pc.ReadFrom(reqBytes)
if err != nil {
log.F("[dns] local read error: %v", err)
pool.PutBuffer(reqBytes)
continue
}
go s.ServePacket(pc, caddr, reqBytes[:n])
}
}
// ServePacket serves dns packet conn.
func (s *Server) ServePacket(pc net.PacketConn, caddr net.Addr, reqBytes []byte) {
respBytes, err := s.Exchange(reqBytes, caddr.String(), false)
defer func() {
pool.PutBuffer(reqBytes)
pool.PutBuffer(respBytes)
}()
if err != nil {
log.F("[dns] error in exchange for %s: %s", caddr, err)
return
}
_, err = pc.WriteTo(respBytes, caddr)
if err != nil {
log.F("[dns] error in local write to %s: %s", caddr, err)
return
}
}
// ListenAndServeTCP listen and serves on tcp port.
func (s *Server) ListenAndServeTCP(wg *sync.WaitGroup) {
l, err := net.Listen("tcp", s.addr)
wg.Done()
if err != nil {
log.F("[dns-tcp] error: %v", err)
return
}
defer l.Close()
log.F("[dns-tcp] listening TCP on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[dns-tcp] error: failed to accept: %v", err)
continue
}
go s.ServeTCP(c)
}
}
// ServeTCP serves a dns tcp connection.
func (s *Server) ServeTCP(c net.Conn) {
defer c.Close()
c.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
var reqLen uint16
if err := binary.Read(c, binary.BigEndian, &reqLen); err != nil {
log.F("[dns-tcp] failed to get request length: %v", err)
return
}
reqBytes := pool.GetBuffer(int(reqLen))
defer pool.PutBuffer(reqBytes)
_, err := io.ReadFull(c, reqBytes)
if err != nil {
log.F("[dns-tcp] error in read reqBytes %s", err)
return
}
respBytes, err := s.Exchange(reqBytes, c.RemoteAddr().String(), true)
defer pool.PutBuffer(respBytes)
if err != nil {
log.F("[dns-tcp] error in exchange: %s", err)
return
}
lenBuf := pool.GetBuffer(2)
defer pool.PutBuffer(lenBuf)
binary.BigEndian.PutUint16(lenBuf, uint16(len(respBytes)))
if _, err := (&net.Buffers{lenBuf, respBytes}).WriteTo(c); err != nil {
log.F("[dns-tcp] error in write respBytes: %s", err)
}
}

46
dns/upstream.go Normal file
View File

@ -0,0 +1,46 @@
package dns
import (
"net"
"sync/atomic"
)
// UPStream is a dns upstream.
type UPStream struct {
index uint32
servers []string
}
// NewUPStream returns a new UpStream.
func NewUPStream(servers []string) *UPStream {
// default port for dns upstream servers
for i, server := range servers {
if _, port, _ := net.SplitHostPort(server); port == "" {
servers[i] = net.JoinHostPort(server, "53")
}
}
return &UPStream{servers: servers}
}
// Server returns a dns server.
func (u *UPStream) Server() string {
return u.servers[atomic.LoadUint32(&u.index)%uint32(len(u.servers))]
}
// Switch switches to the next dns server.
func (u *UPStream) Switch() string {
return u.servers[atomic.AddUint32(&u.index, 1)%uint32(len(u.servers))]
}
// SwitchIf switches to the next dns server if needed.
func (u *UPStream) SwitchIf(server string) string {
if u.Server() == server {
return u.Switch()
}
return u.Server()
}
// Len returns the number of dns servers.
func (u *UPStream) Len() int {
return len(u.servers)
}

27
feature.go Normal file
View File

@ -0,0 +1,27 @@
package main
import (
// comment out the services you don't need to make the compiled binary smaller.
// _ "github.com/nadoo/glider/service/xxx"
// comment out the protocols you don't need to make the compiled binary smaller.
_ "github.com/nadoo/glider/proxy/http"
_ "github.com/nadoo/glider/proxy/kcp"
_ "github.com/nadoo/glider/proxy/mixed"
_ "github.com/nadoo/glider/proxy/obfs"
_ "github.com/nadoo/glider/proxy/pxyproto"
_ "github.com/nadoo/glider/proxy/reject"
_ "github.com/nadoo/glider/proxy/smux"
_ "github.com/nadoo/glider/proxy/socks4"
_ "github.com/nadoo/glider/proxy/socks5"
_ "github.com/nadoo/glider/proxy/ss"
_ "github.com/nadoo/glider/proxy/ssh"
_ "github.com/nadoo/glider/proxy/ssr"
_ "github.com/nadoo/glider/proxy/tcp"
_ "github.com/nadoo/glider/proxy/tls"
_ "github.com/nadoo/glider/proxy/trojan"
_ "github.com/nadoo/glider/proxy/udp"
_ "github.com/nadoo/glider/proxy/vless"
_ "github.com/nadoo/glider/proxy/vmess"
_ "github.com/nadoo/glider/proxy/ws"
)

12
feature_linux.go Normal file
View File

@ -0,0 +1,12 @@
package main
import (
// comment out the services you don't need to make the compiled binary smaller.
_ "github.com/nadoo/glider/service/dhcpd"
// comment out the protocols you don't need to make the compiled binary smaller.
_ "github.com/nadoo/glider/proxy/redir"
_ "github.com/nadoo/glider/proxy/tproxy"
_ "github.com/nadoo/glider/proxy/unix"
_ "github.com/nadoo/glider/proxy/vsock"
)

29
go.mod Normal file
View File

@ -0,0 +1,29 @@
module github.com/nadoo/glider
go 1.24
require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da
github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d
github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb
github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
github.com/nadoo/conflag v0.3.1
github.com/nadoo/ipset v0.5.0
github.com/xtaci/kcp-go/v5 v5.6.18
golang.org/x/crypto v0.35.0
golang.org/x/sys v0.30.0
)
require (
github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/reedsolomon v1.12.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/templexxx/cpu v0.1.1 // indirect
github.com/templexxx/xorsimd v0.4.3 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
golang.org/x/net v0.35.0 // indirect
)

129
go.sum Normal file
View File

@ -0,0 +1,129 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d h1:CPqTNIigGweVPT4CYb+OO2E6XyRKFOmvTHwWRLgCAlE=
github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d/go.mod h1:QX5ZVULjAfZJux/W62Y91HvCh9hyW6enAwcrrv/sLj0=
github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb h1:zXpN5126w/mhECTkqazBkrOJIMatbPP71aSIDR5UuW4=
github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb/go.mod h1:F7WkpqJj9t98ePxB/WJGQTIDeOVPuSJ3qdn6JUjg170=
github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152 h1:ED31mPIxDJnrLt9W9dH5xgd/6KjzEACKHBVGQ33czc0=
github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152/go.mod h1:I9fhc/EvSg88cDxmfQ47v35Ssz9rlFunL/KY0A1JAYI=
github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 h1:fBHFH+Y/GPGFGo7LIrErQc3p2MeAhoIQNgaxPWYsSxk=
github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:ucvhdsUCE3TH0LoLRb6ShHiJl8e39dGlx6A4g/ujlow=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA=
github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU=
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/nadoo/conflag v0.3.1 h1:4pHkLIz8PUsfg6ajNYRRSY3bt6m2LPsu6KOzn5uIXQw=
github.com/nadoo/conflag v0.3.1/go.mod h1:dzFfDUpXdr2uS2oV+udpy5N2vfNOu/bFzjhX1WI52co=
github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc=
github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
github.com/templexxx/xorsimd v0.4.3/go.mod h1:oZQcD6RFDisW2Am58dSAGwwL6rHjbzrlu25VDqfWkQg=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/xtaci/kcp-go/v5 v5.6.18 h1:7oV4mc272pcnn39/13BB11Bx7hJM4ogMIEokJYVWn4g=
github.com/xtaci/kcp-go/v5 v5.6.18/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

78
ipset/ipset_linux.go Normal file
View File

@ -0,0 +1,78 @@
package ipset
import (
"net/netip"
"strings"
"sync"
"github.com/nadoo/ipset"
"github.com/nadoo/glider/rule"
)
// Manager struct.
type Manager struct {
domainSet sync.Map
}
// NewManager returns a Manager
func NewManager(rules []*rule.Config) (*Manager, error) {
if err := ipset.Init(); err != nil {
return nil, err
}
m := &Manager{}
sets := make(map[string]struct{})
for _, r := range rules {
if r.IPSet == "" {
continue
}
if _, ok := sets[r.IPSet]; !ok {
sets[r.IPSet] = struct{}{}
ipset.Create(r.IPSet)
ipset.Flush(r.IPSet)
ipset.Create(r.IPSet+"6", ipset.OptIPv6())
ipset.Flush(r.IPSet + "6")
}
for _, domain := range r.Domain {
m.domainSet.Store(domain, r.IPSet)
}
for _, ip := range r.IP {
addToSet(r.IPSet, ip)
}
for _, cidr := range r.CIDR {
addToSet(r.IPSet, cidr)
}
}
return m, nil
}
// AddDomainIP implements the dns AnswerHandler function, used to update ipset according to domainSet rule.
func (m *Manager) AddDomainIP(domain string, ip netip.Addr) error {
domain = strings.ToLower(domain)
for i := len(domain); i != -1; {
i = strings.LastIndexByte(domain[:i], '.')
if setName, ok := m.domainSet.Load(domain[i+1:]); ok {
addAddrToSet(setName.(string), ip)
}
}
return nil
}
func addToSet(s, item string) error {
if strings.IndexByte(item, '.') == -1 {
return ipset.Add(s+"6", item)
}
return ipset.Add(s, item)
}
func addAddrToSet(s string, ip netip.Addr) error {
if ip.Is4() {
return ipset.AddAddr(s, ip)
}
return ipset.AddAddr(s+"6", ip)
}

23
ipset/ipset_other.go Normal file
View File

@ -0,0 +1,23 @@
//go:build !linux
package ipset
import (
"errors"
"net/netip"
"github.com/nadoo/glider/rule"
)
// Manager struct
type Manager struct{}
// NewManager returns a Manager
func NewManager(rules []*rule.Config) (*Manager, error) {
return nil, errors.New("ipset not supported on this os")
}
// AddDomainIP implements the DNSAnswerHandler function
func (m *Manager) AddDomainIP(domain string, ip netip.Addr) error {
return errors.New("ipset not supported on this os")
}

View File

@ -1,482 +0,0 @@
// Apache License 2.0
// @mdlayher https://github.com/mdlayher/netlink
// Ref: https://github.com/vishvananda/netlink/blob/master/nl/nl_linux.go
package main
import (
"bytes"
"encoding/binary"
"net"
"strings"
"sync"
"sync/atomic"
"syscall"
"unsafe"
"github.com/nadoo/glider/common/log"
)
// NFNL_SUBSYS_IPSET netfilter netlink message types
// https://github.com/torvalds/linux/blob/9e66317d3c92ddaab330c125dfe9d06eee268aff/include/uapi/linux/netfilter/nfnetlink.h#L56
const NFNL_SUBSYS_IPSET = 6
// IPSET_PROTOCOL The protocol version
// http://git.netfilter.org/ipset/tree/include/libipset/linux_ip_set.h
const IPSET_PROTOCOL = 6
// IPSET_MAXNAMELEN The max length of strings including NUL: set and type identifiers
const IPSET_MAXNAMELEN = 32
// Message types and commands
const (
IPSET_CMD_CREATE = 2
IPSET_CMD_FLUSH = 4
IPSET_CMD_ADD = 9
IPSET_CMD_DEL = 10
)
// Attributes at command level
const (
IPSET_ATTR_PROTOCOL = 1 /* 1: Protocol version */
IPSET_ATTR_SETNAME = 2 /* 2: Name of the set */
IPSET_ATTR_TYPENAME = 3 /* 3: Typename */
IPSET_ATTR_REVISION = 4 /* 4: Settype revision */
IPSET_ATTR_FAMILY = 5 /* 5: Settype family */
IPSET_ATTR_DATA = 7 /* 7: Nested attributes */
)
// CADT specific attributes
const (
IPSET_ATTR_IP = 1
IPSET_ATTR_CIDR = 3
)
// IP specific attributes
const (
IPSET_ATTR_IPADDR_IPV4 = 1
IPSET_ATTR_IPADDR_IPV6 = 2
)
// ATTR flags
const (
NLA_F_NESTED = (1 << 15)
NLA_F_NET_BYTEORDER = (1 << 14)
)
var nextSeqNr uint32
var nativeEndian binary.ByteOrder
// IPSetManager struct
type IPSetManager struct {
fd int
lsa syscall.SockaddrNetlink
mainSet string
domainSet sync.Map
}
// NewIPSetManager returns a IPSetManager
func NewIPSetManager(mainSet string, rules []*RuleConf) (*IPSetManager, error) {
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_NETFILTER)
if err != nil {
log.F("%s", err)
return nil, err
}
// defer syscall.Close(fd)
lsa := syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
}
if err = syscall.Bind(fd, &lsa); err != nil {
log.F("%s", err)
return nil, err
}
m := &IPSetManager{fd: fd, lsa: lsa, mainSet: mainSet}
CreateSet(fd, lsa, mainSet)
for _, r := range rules {
set := r.IPSet
if set != "" && set != m.mainSet {
CreateSet(fd, lsa, set)
} else {
set = m.mainSet
}
for _, domain := range r.Domain {
m.domainSet.Store(domain, set)
}
for _, ip := range r.IP {
AddToSet(fd, lsa, mainSet, ip)
AddToSet(fd, lsa, r.IPSet, ip)
}
for _, cidr := range r.CIDR {
AddToSet(fd, lsa, mainSet, cidr)
AddToSet(fd, lsa, r.IPSet, cidr)
}
}
return m, nil
}
// AddDomainIP implements the DNSAnswerHandler function, used to update ipset according to domainSet rule
func (m *IPSetManager) AddDomainIP(domain, ip string) error {
if ip != "" {
domainParts := strings.Split(domain, ".")
length := len(domainParts)
for i := length - 2; i >= 0; i-- {
domain := strings.Join(domainParts[i:length], ".")
// find in domainMap
if ipset, ok := m.domainSet.Load(domain); ok {
AddToSet(m.fd, m.lsa, m.mainSet, ip)
if ipset.(string) != m.mainSet {
AddToSet(m.fd, m.lsa, ipset.(string), ip)
}
}
}
}
return nil
}
// CreateSet create a ipset
func CreateSet(fd int, lsa syscall.SockaddrNetlink, setName string) {
if setName == "" {
return
}
if len(setName) > IPSET_MAXNAMELEN {
log.Fatal("ipset: name too long")
}
log.F("ipset create %s hash:net", setName)
req := NewNetlinkRequest(IPSET_CMD_CREATE|(NFNL_SUBSYS_IPSET<<8), syscall.NLM_F_REQUEST)
// TODO: support AF_INET6
req.AddData(NewNfGenMsg(syscall.AF_INET, 0, 0))
req.AddData(NewRtAttr(IPSET_ATTR_PROTOCOL, Uint8Attr(IPSET_PROTOCOL)))
req.AddData(NewRtAttr(IPSET_ATTR_SETNAME, ZeroTerminated(setName)))
req.AddData(NewRtAttr(IPSET_ATTR_TYPENAME, ZeroTerminated("hash:net")))
req.AddData(NewRtAttr(IPSET_ATTR_REVISION, Uint8Attr(1)))
req.AddData(NewRtAttr(IPSET_ATTR_FAMILY, Uint8Attr(2)))
req.AddData(NewRtAttr(IPSET_ATTR_DATA|NLA_F_NESTED, nil))
err := syscall.Sendto(fd, req.Serialize(), 0, &lsa)
if err != nil {
log.F("%s", err)
}
FlushSet(fd, lsa, setName)
}
// FlushSet flush a ipset
func FlushSet(fd int, lsa syscall.SockaddrNetlink, setName string) {
log.F("ipset flush %s", setName)
req := NewNetlinkRequest(IPSET_CMD_FLUSH|(NFNL_SUBSYS_IPSET<<8), syscall.NLM_F_REQUEST)
// TODO: support AF_INET6
req.AddData(NewNfGenMsg(syscall.AF_INET, 0, 0))
req.AddData(NewRtAttr(IPSET_ATTR_PROTOCOL, Uint8Attr(IPSET_PROTOCOL)))
req.AddData(NewRtAttr(IPSET_ATTR_SETNAME, ZeroTerminated(setName)))
err := syscall.Sendto(fd, req.Serialize(), 0, &lsa)
if err != nil {
log.F("%s", err)
}
}
// AddToSet adds an entry to ipset
func AddToSet(fd int, lsa syscall.SockaddrNetlink, setName, entry string) {
if setName == "" {
return
}
if len(setName) > IPSET_MAXNAMELEN {
log.F("ipset: name too long")
}
log.F("ipset add %s %s", setName, entry)
var ip net.IP
var cidr *net.IPNet
ip, cidr, err := net.ParseCIDR(entry)
if err != nil {
ip = net.ParseIP(entry)
}
if ip == nil {
log.F("ipset: parse %s error", entry)
return
}
req := NewNetlinkRequest(IPSET_CMD_ADD|(NFNL_SUBSYS_IPSET<<8), syscall.NLM_F_REQUEST)
// TODO: support AF_INET6
req.AddData(NewNfGenMsg(syscall.AF_INET, 0, 0))
req.AddData(NewRtAttr(IPSET_ATTR_PROTOCOL, Uint8Attr(IPSET_PROTOCOL)))
req.AddData(NewRtAttr(IPSET_ATTR_SETNAME, ZeroTerminated(setName)))
attrNested := NewRtAttr(IPSET_ATTR_DATA|NLA_F_NESTED, nil)
attrIP := NewRtAttrChild(attrNested, IPSET_ATTR_IP|NLA_F_NESTED, nil)
// TODO: support ipV6
NewRtAttrChild(attrIP, IPSET_ATTR_IPADDR_IPV4|NLA_F_NET_BYTEORDER, ip.To4())
// for cidr prefix
if cidr != nil {
cidrPrefix, _ := cidr.Mask.Size()
NewRtAttrChild(attrNested, IPSET_ATTR_CIDR, Uint8Attr(uint8(cidrPrefix)))
}
NewRtAttrChild(attrNested, 9|NLA_F_NET_BYTEORDER, Uint32Attr(0))
req.AddData(attrNested)
err = syscall.Sendto(fd, req.Serialize(), 0, &lsa)
if err != nil {
log.F("%s", err)
}
}
// NativeEndian get native endianness for the system
func NativeEndian() binary.ByteOrder {
if nativeEndian == nil {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
}
}
return nativeEndian
}
func rtaAlignOf(attrlen int) int {
return (attrlen + syscall.RTA_ALIGNTO - 1) & ^(syscall.RTA_ALIGNTO - 1)
}
// NetlinkRequestData .
type NetlinkRequestData interface {
Len() int
Serialize() []byte
}
// NfGenMsg .
type NfGenMsg struct {
nfgenFamily uint8
version uint8
resID uint16
}
// NewNfGenMsg .
func NewNfGenMsg(nfgenFamily, version, resID int) *NfGenMsg {
return &NfGenMsg{
nfgenFamily: uint8(nfgenFamily),
version: uint8(version),
resID: uint16(resID),
}
}
// Len .
func (m *NfGenMsg) Len() int {
return rtaAlignOf(4)
}
// Serialize .
func (m *NfGenMsg) Serialize() []byte {
native := NativeEndian()
length := m.Len()
buf := make([]byte, rtaAlignOf(length))
buf[0] = m.nfgenFamily
buf[1] = m.version
native.PutUint16(buf[2:4], m.resID)
return buf
}
// RtAttr Extend RtAttr to handle data and children
type RtAttr struct {
syscall.RtAttr
Data []byte
children []NetlinkRequestData
}
// NewRtAttr Create a new Extended RtAttr object
func NewRtAttr(attrType int, data []byte) *RtAttr {
return &RtAttr{
RtAttr: syscall.RtAttr{
Type: uint16(attrType),
},
children: []NetlinkRequestData{},
Data: data,
}
}
// NewRtAttrChild Create a new RtAttr obj anc add it as a child of an existing object
func NewRtAttrChild(parent *RtAttr, attrType int, data []byte) *RtAttr {
attr := NewRtAttr(attrType, data)
parent.children = append(parent.children, attr)
return attr
}
// Len .
func (a *RtAttr) Len() int {
if len(a.children) == 0 {
return (syscall.SizeofRtAttr + len(a.Data))
}
l := 0
for _, child := range a.children {
l += rtaAlignOf(child.Len())
}
l += syscall.SizeofRtAttr
return rtaAlignOf(l + len(a.Data))
}
// Serialize the RtAttr into a byte array
// This can't just unsafe.cast because it must iterate through children.
func (a *RtAttr) Serialize() []byte {
native := NativeEndian()
length := a.Len()
buf := make([]byte, rtaAlignOf(length))
next := 4
if a.Data != nil {
copy(buf[next:], a.Data)
next += rtaAlignOf(len(a.Data))
}
if len(a.children) > 0 {
for _, child := range a.children {
childBuf := child.Serialize()
copy(buf[next:], childBuf)
next += rtaAlignOf(len(childBuf))
}
}
if l := uint16(length); l != 0 {
native.PutUint16(buf[0:2], l)
}
native.PutUint16(buf[2:4], a.Type)
return buf
}
// NetlinkRequest .
type NetlinkRequest struct {
syscall.NlMsghdr
Data []NetlinkRequestData
RawData []byte
}
// NewNetlinkRequest create a new netlink request from proto and flags
// Note the Len value will be inaccurate once data is added until
// the message is serialized
func NewNetlinkRequest(proto, flags int) *NetlinkRequest {
return &NetlinkRequest{
NlMsghdr: syscall.NlMsghdr{
Len: uint32(syscall.SizeofNlMsghdr),
Type: uint16(proto),
Flags: syscall.NLM_F_REQUEST | uint16(flags),
Seq: atomic.AddUint32(&nextSeqNr, 1),
// Pid: uint32(os.Getpid()),
},
}
}
// Serialize the Netlink Request into a byte array
func (req *NetlinkRequest) Serialize() []byte {
length := syscall.SizeofNlMsghdr
dataBytes := make([][]byte, len(req.Data))
for i, data := range req.Data {
dataBytes[i] = data.Serialize()
length = length + len(dataBytes[i])
}
length += len(req.RawData)
req.Len = uint32(length)
b := make([]byte, length)
hdr := (*(*[syscall.SizeofNlMsghdr]byte)(unsafe.Pointer(req)))[:]
next := syscall.SizeofNlMsghdr
copy(b[0:next], hdr)
for _, data := range dataBytes {
for _, dataByte := range data {
b[next] = dataByte
next = next + 1
}
}
// Add the raw data if any
if len(req.RawData) > 0 {
copy(b[next:length], req.RawData)
}
return b
}
// AddData add data to request
func (req *NetlinkRequest) AddData(data NetlinkRequestData) {
if data != nil {
req.Data = append(req.Data, data)
}
}
// AddRawData adds raw bytes to the end of the NetlinkRequest object during serialization
func (req *NetlinkRequest) AddRawData(data []byte) {
if data != nil {
req.RawData = append(req.RawData, data...)
}
}
// Uint8Attr .
func Uint8Attr(v uint8) []byte {
return []byte{byte(v)}
}
// Uint16Attr .
func Uint16Attr(v uint16) []byte {
native := NativeEndian()
bytes := make([]byte, 2)
native.PutUint16(bytes, v)
return bytes
}
// Uint32Attr .
func Uint32Attr(v uint32) []byte {
native := NativeEndian()
bytes := make([]byte, 4)
native.PutUint32(bytes, v)
return bytes
}
// ZeroTerminated .
func ZeroTerminated(s string) []byte {
bytes := make([]byte, len(s)+1)
for i := 0; i < len(s); i++ {
bytes[i] = s[i]
}
bytes[len(s)] = 0
return bytes
}
// NonZeroTerminated .
func NonZeroTerminated(s string) []byte {
bytes := make([]byte, len(s))
for i := 0; i < len(s); i++ {
bytes[i] = s[i]
}
return bytes
}
// BytesToString .
func BytesToString(b []byte) string {
n := bytes.Index(b, []byte{0})
return string(b[:n])
}

View File

@ -1,18 +0,0 @@
// +build !linux
package main
import "errors"
// IPSetManager struct
type IPSetManager struct{}
// NewIPSetManager returns a IPSetManager
func NewIPSetManager(mainSet string, rules []*RuleConf) (*IPSetManager, error) {
return nil, errors.New("ipset not supported on this os")
}
// AddDomainIP implements the DNSAnswerHandler function
func (m *IPSetManager) AddDomainIP(domain, ip string) error {
return errors.New("ipset not supported on this os")
}

126
main.go
View File

@ -1,98 +1,90 @@
package main
import (
stdlog "log"
"context"
"net"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/dns"
"github.com/nadoo/glider/ipset"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
_ "github.com/nadoo/glider/proxy/dnstun"
_ "github.com/nadoo/glider/proxy/http"
_ "github.com/nadoo/glider/proxy/mixed"
_ "github.com/nadoo/glider/proxy/socks5"
_ "github.com/nadoo/glider/proxy/ss"
_ "github.com/nadoo/glider/proxy/ssr"
_ "github.com/nadoo/glider/proxy/tcptun"
_ "github.com/nadoo/glider/proxy/tls"
_ "github.com/nadoo/glider/proxy/udptun"
_ "github.com/nadoo/glider/proxy/uottun"
_ "github.com/nadoo/glider/proxy/vmess"
"github.com/nadoo/glider/rule"
"github.com/nadoo/glider/service"
)
// VERSION .
const VERSION = "0.6.0"
func dialerFromConf() proxy.Dialer {
// global forwarders in xx.conf
var fwdrs []proxy.Dialer
for _, chain := range conf.Forward {
var fwdr proxy.Dialer
var err error
for _, url := range strings.Split(chain, ",") {
fwdr, err = proxy.DialerFromURL(url, fwdr)
if err != nil {
log.Fatal(err)
}
}
fwdrs = append(fwdrs, fwdr)
}
return NewStrategyDialer(conf.Strategy, fwdrs, conf.CheckWebSite, conf.CheckDuration)
}
var (
version = "0.17.0"
config = parseConfig()
)
func main() {
// global rule proxy
pxy := rule.NewProxy(config.Forwards, &config.Strategy, config.rules)
confInit()
// ipset manager
ipsetM, _ := ipset.NewManager(config.rules)
log.F = func(f string, v ...interface{}) {
if conf.Verbose {
stdlog.Printf(f, v...)
}
}
sDialer := NewRuleDialer(conf.rules, dialerFromConf())
for _, listen := range conf.Listen {
local, err := proxy.ServerFromURL(listen, sDialer)
// check and setup dns server
if config.DNS != "" {
d, err := dns.NewServer(config.DNS, pxy, &config.DNSConfig)
if err != nil {
log.Fatal(err)
}
go local.ListenAndServe()
}
ipsetM, err := NewIPSetManager(conf.IPSet, conf.rules)
if err != nil {
log.F("create ipset manager error: %s", err)
}
if conf.DNS != "" {
d, err := dns.NewDNS(conf.DNS, conf.DNSServer[0], sDialer, false)
if err != nil {
log.Fatal(err)
}
// rule
for _, r := range conf.rules {
// rules
for _, r := range config.rules {
if len(r.DNSServers) > 0 {
for _, domain := range r.Domain {
if len(r.DNSServer) > 0 {
d.SetServer(domain, r.DNSServer[0])
d.SetServers(domain, r.DNSServers)
}
}
}
// add a handler to update proxy rules when a domain resolved
d.AddAnswerHandler(sDialer.AddDomainIP)
d.AddHandler(pxy.AddDomainIP)
if ipsetM != nil {
d.AddAnswerHandler(ipsetM.AddDomainIP)
d.AddHandler(ipsetM.AddDomainIP)
}
go d.ListenAndServe()
d.Start()
// custom resolver
net.DefaultResolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: time.Second * 3}
return d.DialContext(ctx, "udp", config.DNS)
},
}
}
for _, r := range config.rules {
r.IP, r.CIDR, r.Domain = nil, nil, nil
}
// enable checkers
pxy.Check()
// run proxy servers
for _, listen := range config.Listens {
local, err := proxy.ServerFromURL(listen, pxy)
if err != nil {
log.Fatal(err)
}
go local.ListenAndServe()
}
// run services
for _, s := range config.Services {
service, err := service.New(s)
if err != nil {
log.Fatal(err)
}
go service.Run()
}
sigCh := make(chan os.Signal, 1)

View File

@ -1,6 +0,0 @@
package main
import (
_ "github.com/nadoo/glider/proxy/redir"
// _ "github.com/nadoo/glider/proxy/tproxy"
)

41
pkg/log/log.go Normal file
View File

@ -0,0 +1,41 @@
package log
import (
"fmt"
stdlog "log"
)
var enable = false
// Set sets the logger's verbose mode and output flags.
func Set(verbose bool, flag int) {
enable = verbose
stdlog.SetFlags(flag)
}
// F prints debug log.
func F(f string, v ...any) {
if enable {
stdlog.Output(2, fmt.Sprintf(f, v...))
}
}
// Print prints log.
func Print(v ...any) {
stdlog.Print(v...)
}
// Printf prints log.
func Printf(f string, v ...any) {
stdlog.Printf(f, v...)
}
// Fatal log and exit.
func Fatal(v ...any) {
stdlog.Fatal(v...)
}
// Fatalf log and exit.
func Fatalf(f string, v ...any) {
stdlog.Fatalf(f, v...)
}

51
pkg/pool/buffer.go Normal file
View File

@ -0,0 +1,51 @@
package pool
import (
"math/bits"
"sync"
"unsafe"
)
const (
// number of pools.
num = 17
maxsize = 1 << (num - 1)
)
var (
sizes [num]int
pools [num]sync.Pool
)
func init() {
for i := range num {
size := 1 << i
sizes[i] = size
pools[i].New = func() any {
buf := make([]byte, size)
return unsafe.SliceData(buf)
}
}
}
// GetBuffer gets a buffer from pool, size should in range: [1, 65536],
// otherwise, this function will call make([]byte, size) directly.
func GetBuffer(size int) []byte {
if size >= 1 && size <= maxsize {
i := bits.Len32(uint32(size - 1))
if p := pools[i].Get().(*byte); p != nil {
return unsafe.Slice(p, 1<<i)[:size]
}
}
return make([]byte, size)
}
// PutBuffer puts a buffer into pool.
func PutBuffer(buf []byte) {
if size := cap(buf); size >= 1 && size <= maxsize {
i := bits.Len32(uint32(size - 1))
if sizes[i] == size {
pools[i].Put(unsafe.SliceData(buf))
}
}
}

24
pkg/pool/bufreader.go Normal file
View File

@ -0,0 +1,24 @@
package pool
import (
"bufio"
"io"
"sync"
)
var bufReaderPool sync.Pool
// GetBufReader returns a *bufio.Reader from pool.
func GetBufReader(r io.Reader) *bufio.Reader {
if v := bufReaderPool.Get(); v != nil {
br := v.(*bufio.Reader)
br.Reset(r)
return br
}
return bufio.NewReader(r)
}
// PutBufReader puts a *bufio.Reader into pool.
func PutBufReader(br *bufio.Reader) {
bufReaderPool.Put(br)
}

23
pkg/pool/bytesbuffer.go Normal file
View File

@ -0,0 +1,23 @@
package pool
import (
"bytes"
"sync"
)
var bytesBufPool = sync.Pool{
New: func() any { return &bytes.Buffer{} },
}
// GetBytesBuffer returns a bytes.buffer from pool.
func GetBytesBuffer() *bytes.Buffer {
return bytesBufPool.Get().(*bytes.Buffer)
}
// PutBytesBuffer puts a bytes.buffer into pool.
func PutBytesBuffer(buf *bytes.Buffer) {
if buf.Cap() <= 64<<10 {
buf.Reset()
bytesBufPool.Put(buf)
}
}

106
pkg/smux/frame.go Normal file
View File

@ -0,0 +1,106 @@
// MIT License
//
// Copyright (c) 2016-2017 xtaci
//
// 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:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// 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.
package smux
import (
"encoding/binary"
"fmt"
)
const ( // cmds
// protocol version 1:
cmdSYN byte = iota // stream open
cmdFIN // stream close, a.k.a EOF mark
cmdPSH // data push
cmdNOP // no operation
// protocol version 2 extra commands
// notify bytes consumed by remote peer-end
cmdUPD
)
const (
// data size of cmdUPD, format:
// |4B data consumed(ACK)| 4B window size(WINDOW) |
szCmdUPD = 8
)
const (
// initial peer window guess, a slow-start
initialPeerWindow = 262144
)
const (
sizeOfVer = 1
sizeOfCmd = 1
sizeOfLength = 2
sizeOfSid = 4
headerSize = sizeOfVer + sizeOfCmd + sizeOfSid + sizeOfLength
)
// Frame defines a packet from or to be multiplexed into a single connection
type Frame struct {
ver byte // version
cmd byte // command
sid uint32 // stream id
data []byte // payload
}
// newFrame creates a new frame with given version, command and stream id
func newFrame(version byte, cmd byte, sid uint32) Frame {
return Frame{ver: version, cmd: cmd, sid: sid}
}
// rawHeader is a byte array representation of Frame header
type rawHeader [headerSize]byte
func (h rawHeader) Version() byte {
return h[0]
}
func (h rawHeader) Cmd() byte {
return h[1]
}
func (h rawHeader) Length() uint16 {
return binary.LittleEndian.Uint16(h[2:])
}
func (h rawHeader) StreamID() uint32 {
return binary.LittleEndian.Uint32(h[4:])
}
func (h rawHeader) String() string {
return fmt.Sprintf("Version:%d Cmd:%d StreamID:%d Length:%d",
h.Version(), h.Cmd(), h.StreamID(), h.Length())
}
// updHeader is a byte array representation of cmdUPD
type updHeader [szCmdUPD]byte
func (h updHeader) Consumed() uint32 {
return binary.LittleEndian.Uint32(h[:])
}
func (h updHeader) Window() uint32 {
return binary.LittleEndian.Uint32(h[4:])
}

128
pkg/smux/mux.go Normal file
View File

@ -0,0 +1,128 @@
// MIT License
//
// Copyright (c) 2016-2017 xtaci
//
// 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:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// 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.
package smux
import (
"errors"
"fmt"
"io"
"math"
"time"
)
// Config is used to tune the Smux session
type Config struct {
// SMUX Protocol version, support 1,2
Version int
// Disabled keepalive
KeepAliveDisabled bool
// KeepAliveInterval is how often to send a NOP command to the remote
KeepAliveInterval time.Duration
// KeepAliveTimeout is how long the session
// will be closed if no data has arrived
KeepAliveTimeout time.Duration
// MaxFrameSize is used to control the maximum
// frame size to sent to the remote
MaxFrameSize int
// MaxReceiveBuffer is used to control the maximum
// number of data in the buffer pool
MaxReceiveBuffer int
// MaxStreamBuffer is used to control the maximum
// number of data per stream
MaxStreamBuffer int
}
// DefaultConfig is used to return a default configuration
func DefaultConfig() *Config {
return &Config{
Version: 1,
KeepAliveInterval: 10 * time.Second,
KeepAliveTimeout: 30 * time.Second,
MaxFrameSize: 32768,
MaxReceiveBuffer: 4194304,
MaxStreamBuffer: 65536,
}
}
// VerifyConfig is used to verify the sanity of configuration
func VerifyConfig(config *Config) error {
if !(config.Version == 1 || config.Version == 2) {
return errors.New("unsupported protocol version")
}
if !config.KeepAliveDisabled {
if config.KeepAliveInterval == 0 {
return errors.New("keep-alive interval must be positive")
}
if config.KeepAliveTimeout < config.KeepAliveInterval {
return fmt.Errorf("keep-alive timeout must be larger than keep-alive interval")
}
}
if config.MaxFrameSize <= 0 {
return errors.New("max frame size must be positive")
}
if config.MaxFrameSize > 65535 {
return errors.New("max frame size must not be larger than 65535")
}
if config.MaxReceiveBuffer <= 0 {
return errors.New("max receive buffer must be positive")
}
if config.MaxStreamBuffer <= 0 {
return errors.New("max stream buffer must be positive")
}
if config.MaxStreamBuffer > config.MaxReceiveBuffer {
return errors.New("max stream buffer must not be larger than max receive buffer")
}
if config.MaxStreamBuffer > math.MaxInt32 {
return errors.New("max stream buffer cannot be larger than 2147483647")
}
return nil
}
// Server is used to initialize a new server-side connection.
func Server(conn io.ReadWriteCloser, config *Config) (*Session, error) {
if config == nil {
config = DefaultConfig()
}
if err := VerifyConfig(config); err != nil {
return nil, err
}
return newSession(config, conn, false), nil
}
// Client is used to initialize a new client-side connection.
func Client(conn io.ReadWriteCloser, config *Config) (*Session, error) {
if config == nil {
config = DefaultConfig()
}
if err := VerifyConfig(config); err != nil {
return nil, err
}
return newSession(config, conn, true), nil
}

621
pkg/smux/session.go Normal file
View File

@ -0,0 +1,621 @@
// MIT License
//
// Copyright (c) 2016-2017 xtaci
//
// 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:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// 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.
package smux
import (
"container/heap"
"encoding/binary"
"errors"
"io"
"net"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/nadoo/glider/pkg/pool"
)
const (
defaultAcceptBacklog = 1024
maxShaperSize = 1024
openCloseTimeout = 30 * time.Second // Timeout for opening/closing streams
)
// CLASSID represents the class of a frame
type CLASSID int
const (
CLSCTRL CLASSID = iota // prioritized control signal
CLSDATA
)
// timeoutError representing timeouts for operations such as accept, read and write
//
// To better cooperate with the standard library, timeoutError should implement the standard library's `net.Error`.
//
// For example, using smux to implement net.Listener and work with http.Server, the keep-alive connection (*smux.Stream) will be unexpectedly closed.
// For more details, see https://github.com/xtaci/smux/pull/99.
type timeoutError struct{}
func (timeoutError) Error() string { return "timeout" }
func (timeoutError) Temporary() bool { return true }
func (timeoutError) Timeout() bool { return true }
var (
ErrInvalidProtocol = errors.New("invalid protocol")
ErrConsumed = errors.New("peer consumed more than sent")
ErrGoAway = errors.New("stream id overflows, should start a new connection")
ErrTimeout net.Error = &timeoutError{}
ErrWouldBlock = errors.New("operation would block on IO")
)
// writeRequest represents a request to write a frame
type writeRequest struct {
class CLASSID
frame Frame
seq uint32
result chan writeResult
}
// writeResult represents the result of a write request
type writeResult struct {
n int
err error
}
// Session defines a multiplexed connection for streams
type Session struct {
conn io.ReadWriteCloser
config *Config
nextStreamID uint32 // next stream identifier
nextStreamIDLock sync.Mutex
bucket int32 // token bucket
bucketNotify chan struct{} // used for waiting for tokens
streams map[uint32]*stream // all streams in this session
streamLock sync.Mutex // locks streams
die chan struct{} // flag session has died
dieOnce sync.Once
// socket error handling
socketReadError atomic.Value
socketWriteError atomic.Value
chSocketReadError chan struct{}
chSocketWriteError chan struct{}
socketReadErrorOnce sync.Once
socketWriteErrorOnce sync.Once
// smux protocol errors
protoError atomic.Value
chProtoError chan struct{}
protoErrorOnce sync.Once
chAccepts chan *stream
dataReady int32 // flag data has arrived
goAway int32 // flag id exhausted
deadline atomic.Value
requestID uint32 // Monotonic increasing write request ID
shaper chan writeRequest // a shaper for writing
writes chan writeRequest
}
func newSession(config *Config, conn io.ReadWriteCloser, client bool) *Session {
s := new(Session)
s.die = make(chan struct{})
s.conn = conn
s.config = config
s.streams = make(map[uint32]*stream)
s.chAccepts = make(chan *stream, defaultAcceptBacklog)
s.bucket = int32(config.MaxReceiveBuffer)
s.bucketNotify = make(chan struct{}, 1)
s.shaper = make(chan writeRequest)
s.writes = make(chan writeRequest)
s.chSocketReadError = make(chan struct{})
s.chSocketWriteError = make(chan struct{})
s.chProtoError = make(chan struct{})
if client {
s.nextStreamID = 1
} else {
s.nextStreamID = 0
}
go s.shaperLoop()
go s.recvLoop()
go s.sendLoop()
if !config.KeepAliveDisabled {
go s.keepalive()
}
return s
}
// OpenStream is used to create a new stream
func (s *Session) OpenStream() (*Stream, error) {
if s.IsClosed() {
return nil, io.ErrClosedPipe
}
// generate stream id
s.nextStreamIDLock.Lock()
if s.goAway > 0 {
s.nextStreamIDLock.Unlock()
return nil, ErrGoAway
}
s.nextStreamID += 2
sid := s.nextStreamID
if sid == sid%2 { // stream-id overflows
s.goAway = 1
s.nextStreamIDLock.Unlock()
return nil, ErrGoAway
}
s.nextStreamIDLock.Unlock()
stream := newStream(sid, s.config.MaxFrameSize, s)
if _, err := s.writeControlFrame(newFrame(byte(s.config.Version), cmdSYN, sid)); err != nil {
return nil, err
}
s.streamLock.Lock()
defer s.streamLock.Unlock()
select {
case <-s.chSocketReadError:
return nil, s.socketReadError.Load().(error)
case <-s.chSocketWriteError:
return nil, s.socketWriteError.Load().(error)
case <-s.die:
return nil, io.ErrClosedPipe
default:
s.streams[sid] = stream
wrapper := &Stream{stream: stream}
// NOTE(x): disabled finalizer for issue #997
/*
runtime.SetFinalizer(wrapper, func(s *Stream) {
s.Close()
})
*/
return wrapper, nil
}
}
// Open returns a generic ReadWriteCloser
func (s *Session) Open() (io.ReadWriteCloser, error) {
return s.OpenStream()
}
// AcceptStream is used to block until the next available stream
// is ready to be accepted.
func (s *Session) AcceptStream() (*Stream, error) {
var deadline <-chan time.Time
if d, ok := s.deadline.Load().(time.Time); ok && !d.IsZero() {
timer := time.NewTimer(time.Until(d))
defer timer.Stop()
deadline = timer.C
}
select {
case stream := <-s.chAccepts:
wrapper := &Stream{stream: stream}
runtime.SetFinalizer(wrapper, func(s *Stream) {
s.Close()
})
return wrapper, nil
case <-deadline:
return nil, ErrTimeout
case <-s.chSocketReadError:
return nil, s.socketReadError.Load().(error)
case <-s.chProtoError:
return nil, s.protoError.Load().(error)
case <-s.die:
return nil, io.ErrClosedPipe
}
}
// Accept Returns a generic ReadWriteCloser instead of smux.Stream
func (s *Session) Accept() (io.ReadWriteCloser, error) {
return s.AcceptStream()
}
// Close is used to close the session and all streams.
func (s *Session) Close() error {
var once bool
s.dieOnce.Do(func() {
close(s.die)
once = true
})
if once {
s.streamLock.Lock()
for k := range s.streams {
s.streams[k].sessionClose()
}
s.streamLock.Unlock()
return s.conn.Close()
} else {
return io.ErrClosedPipe
}
}
// CloseChan can be used by someone who wants to be notified immediately when this
// session is closed
func (s *Session) CloseChan() <-chan struct{} {
return s.die
}
// notifyBucket notifies recvLoop that bucket is available
func (s *Session) notifyBucket() {
select {
case s.bucketNotify <- struct{}{}:
default:
}
}
func (s *Session) notifyReadError(err error) {
s.socketReadErrorOnce.Do(func() {
s.socketReadError.Store(err)
close(s.chSocketReadError)
})
}
func (s *Session) notifyWriteError(err error) {
s.socketWriteErrorOnce.Do(func() {
s.socketWriteError.Store(err)
close(s.chSocketWriteError)
})
}
func (s *Session) notifyProtoError(err error) {
s.protoErrorOnce.Do(func() {
s.protoError.Store(err)
close(s.chProtoError)
})
}
// IsClosed does a safe check to see if we have shutdown
func (s *Session) IsClosed() bool {
select {
case <-s.die:
return true
default:
return false
}
}
// NumStreams returns the number of currently open streams
func (s *Session) NumStreams() int {
if s.IsClosed() {
return 0
}
s.streamLock.Lock()
defer s.streamLock.Unlock()
return len(s.streams)
}
// SetDeadline sets a deadline used by Accept* calls.
// A zero time value disables the deadline.
func (s *Session) SetDeadline(t time.Time) error {
s.deadline.Store(t)
return nil
}
// LocalAddr satisfies net.Conn interface
func (s *Session) LocalAddr() net.Addr {
if ts, ok := s.conn.(interface {
LocalAddr() net.Addr
}); ok {
return ts.LocalAddr()
}
return nil
}
// RemoteAddr satisfies net.Conn interface
func (s *Session) RemoteAddr() net.Addr {
if ts, ok := s.conn.(interface {
RemoteAddr() net.Addr
}); ok {
return ts.RemoteAddr()
}
return nil
}
// notify the session that a stream has closed
func (s *Session) streamClosed(sid uint32) {
s.streamLock.Lock()
if stream, ok := s.streams[sid]; ok {
n := stream.recycleTokens()
if n > 0 { // return remaining tokens to the bucket
if atomic.AddInt32(&s.bucket, int32(n)) > 0 {
s.notifyBucket()
}
}
delete(s.streams, sid)
}
s.streamLock.Unlock()
}
// returnTokens is called by stream to return token after read
func (s *Session) returnTokens(n int) {
if atomic.AddInt32(&s.bucket, int32(n)) > 0 {
s.notifyBucket()
}
}
// recvLoop keeps on reading from underlying connection if tokens are available
func (s *Session) recvLoop() {
var hdr rawHeader
var updHdr updHeader
for {
for atomic.LoadInt32(&s.bucket) <= 0 && !s.IsClosed() {
select {
case <-s.bucketNotify:
case <-s.die:
return
}
}
// read header first
if _, err := io.ReadFull(s.conn, hdr[:]); err == nil {
atomic.StoreInt32(&s.dataReady, 1)
if hdr.Version() != byte(s.config.Version) {
s.notifyProtoError(ErrInvalidProtocol)
return
}
sid := hdr.StreamID()
switch hdr.Cmd() {
case cmdNOP:
case cmdSYN: // stream opening
s.streamLock.Lock()
if _, ok := s.streams[sid]; !ok {
stream := newStream(sid, s.config.MaxFrameSize, s)
s.streams[sid] = stream
select {
case s.chAccepts <- stream:
case <-s.die:
}
}
s.streamLock.Unlock()
case cmdFIN: // stream closing
s.streamLock.Lock()
if stream, ok := s.streams[sid]; ok {
stream.fin()
stream.notifyReadEvent()
}
s.streamLock.Unlock()
case cmdPSH: // data frame
if hdr.Length() > 0 {
newbuf := pool.GetBuffer(int(hdr.Length()))
if written, err := io.ReadFull(s.conn, newbuf); err == nil {
s.streamLock.Lock()
if stream, ok := s.streams[sid]; ok {
stream.pushBytes(newbuf)
// a stream used some token
atomic.AddInt32(&s.bucket, -int32(written))
stream.notifyReadEvent()
} else {
// data directed to a missing/closed stream, recycle the buffer immediately.
pool.PutBuffer(newbuf)
}
s.streamLock.Unlock()
} else {
s.notifyReadError(err)
return
}
}
case cmdUPD: // a window update signal
if _, err := io.ReadFull(s.conn, updHdr[:]); err == nil {
s.streamLock.Lock()
if stream, ok := s.streams[sid]; ok {
stream.update(updHdr.Consumed(), updHdr.Window())
}
s.streamLock.Unlock()
} else {
s.notifyReadError(err)
return
}
default:
s.notifyProtoError(ErrInvalidProtocol)
return
}
} else {
s.notifyReadError(err)
return
}
}
}
// keepalive sends NOP frame to peer to keep the connection alive, and detect dead peers
func (s *Session) keepalive() {
tickerPing := time.NewTicker(s.config.KeepAliveInterval)
tickerTimeout := time.NewTicker(s.config.KeepAliveTimeout)
defer tickerPing.Stop()
defer tickerTimeout.Stop()
for {
select {
case <-tickerPing.C:
s.writeFrameInternal(newFrame(byte(s.config.Version), cmdNOP, 0), tickerPing.C, CLSCTRL)
s.notifyBucket() // force a signal to the recvLoop
case <-tickerTimeout.C:
if !atomic.CompareAndSwapInt32(&s.dataReady, 1, 0) {
// recvLoop may block while bucket is 0, in this case,
// session should not be closed.
if atomic.LoadInt32(&s.bucket) > 0 {
s.Close()
return
}
}
case <-s.die:
return
}
}
}
// shaperLoop implements a priority queue for write requests,
// some control messages are prioritized over data messages
func (s *Session) shaperLoop() {
var reqs shaperHeap
var next writeRequest
var chWrite chan writeRequest
var chShaper chan writeRequest
for {
// chWrite is not available until it has packet to send
if len(reqs) > 0 {
chWrite = s.writes
next = heap.Pop(&reqs).(writeRequest)
} else {
chWrite = nil
}
// control heap size, chShaper is not available until packets are less than maximum allowed
if len(reqs) >= maxShaperSize {
chShaper = nil
} else {
chShaper = s.shaper
}
// assertion on non nil
if chShaper == nil && chWrite == nil {
panic("both channel are nil")
}
select {
case <-s.die:
return
case r := <-chShaper:
if chWrite != nil { // next is valid, reshape
heap.Push(&reqs, next)
}
heap.Push(&reqs, r)
case chWrite <- next:
}
}
}
// sendLoop sends frames to the underlying connection
func (s *Session) sendLoop() {
var buf []byte
var n int
var err error
var vec [][]byte // vector for writeBuffers
bw, ok := s.conn.(interface {
WriteBuffers(v [][]byte) (n int, err error)
})
if ok {
buf = make([]byte, headerSize)
vec = make([][]byte, 2)
} else {
buf = make([]byte, (1<<16)+headerSize)
}
for {
select {
case <-s.die:
return
case request := <-s.writes:
buf[0] = request.frame.ver
buf[1] = request.frame.cmd
binary.LittleEndian.PutUint16(buf[2:], uint16(len(request.frame.data)))
binary.LittleEndian.PutUint32(buf[4:], request.frame.sid)
// support for scatter-gather I/O
if len(vec) > 0 {
vec[0] = buf[:headerSize]
vec[1] = request.frame.data
n, err = bw.WriteBuffers(vec)
} else {
copy(buf[headerSize:], request.frame.data)
n, err = s.conn.Write(buf[:headerSize+len(request.frame.data)])
}
n -= headerSize
if n < 0 {
n = 0
}
result := writeResult{
n: n,
err: err,
}
request.result <- result
close(request.result)
// store conn error
if err != nil {
s.notifyWriteError(err)
return
}
}
}
}
// writeControlFrame writes the control frame to the underlying connection
// and returns the number of bytes written if successful
func (s *Session) writeControlFrame(f Frame) (n int, err error) {
timer := time.NewTimer(openCloseTimeout)
defer timer.Stop()
return s.writeFrameInternal(f, timer.C, CLSCTRL)
}
// internal writeFrame version to support deadline used in keepalive
func (s *Session) writeFrameInternal(f Frame, deadline <-chan time.Time, class CLASSID) (int, error) {
req := writeRequest{
class: class,
frame: f,
seq: atomic.AddUint32(&s.requestID, 1),
result: make(chan writeResult, 1),
}
select {
case s.shaper <- req:
case <-s.die:
return 0, io.ErrClosedPipe
case <-s.chSocketWriteError:
return 0, s.socketWriteError.Load().(error)
case <-deadline:
return 0, ErrTimeout
}
select {
case result := <-req.result:
return result.n, result.err
case <-s.die:
return 0, io.ErrClosedPipe
case <-s.chSocketWriteError:
return 0, s.socketWriteError.Load().(error)
case <-deadline:
return 0, ErrTimeout
}
}

56
pkg/smux/shaper.go Normal file
View File

@ -0,0 +1,56 @@
// MIT License
//
// Copyright (c) 2016-2017 xtaci
//
// 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:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// 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.
package smux
// _itimediff returns the time difference between two uint32 values.
// The result is a signed 32-bit integer representing the difference between 'later' and 'earlier'.
func _itimediff(later, earlier uint32) int32 {
return (int32)(later - earlier)
}
// shaperHeap is a min-heap of writeRequest.
// It orders writeRequests by class first, then by sequence number within the same class.
type shaperHeap []writeRequest
func (h shaperHeap) Len() int { return len(h) }
// Less determines the ordering of elements in the heap.
// Requests are ordered by their class first. If two requests have the same class,
// they are ordered by their sequence numbers.
func (h shaperHeap) Less(i, j int) bool {
if h[i].class != h[j].class {
return h[i].class < h[j].class
}
return _itimediff(h[j].seq, h[i].seq) > 0
}
func (h shaperHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *shaperHeap) Push(x interface{}) { *h = append(*h, x.(writeRequest)) }
func (h *shaperHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}

615
pkg/smux/stream.go Normal file
View File

@ -0,0 +1,615 @@
// MIT License
//
// Copyright (c) 2016-2017 xtaci
//
// 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:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// 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.
package smux
import (
"encoding/binary"
"io"
"net"
"sync"
"sync/atomic"
"time"
"github.com/nadoo/glider/pkg/pool"
)
// wrapper for GC
type Stream struct {
*stream
}
// Stream implements net.Conn
type stream struct {
id uint32 // Stream identifier
sess *Session
buffers [][]byte // the sequential buffers of stream
heads [][]byte // slice heads of the buffers above, kept for recycle
bufferLock sync.Mutex // Mutex to protect access to buffers
frameSize int // Maximum frame size for the stream
// notify a read event
chReadEvent chan struct{}
// flag the stream has closed
die chan struct{}
dieOnce sync.Once // Ensures die channel is closed only once
// FIN command
chFinEvent chan struct{}
finEventOnce sync.Once // Ensures chFinEvent is closed only once
// deadlines
readDeadline atomic.Value
writeDeadline atomic.Value
// per stream sliding window control
numRead uint32 // count num of bytes read
numWritten uint32 // count num of bytes written
incr uint32 // bytes sent since last window update
// UPD command
peerConsumed uint32 // num of bytes the peer has consumed
peerWindow uint32 // peer window, initialized to 256KB, updated by peer
chUpdate chan struct{} // notify of remote data consuming and window update
}
// newStream initializes and returns a new Stream.
func newStream(id uint32, frameSize int, sess *Session) *stream {
s := new(stream)
s.id = id
s.chReadEvent = make(chan struct{}, 1)
s.chUpdate = make(chan struct{}, 1)
s.frameSize = frameSize
s.sess = sess
s.die = make(chan struct{})
s.chFinEvent = make(chan struct{})
s.peerWindow = initialPeerWindow // set to initial window size
return s
}
// ID returns the stream's unique identifier.
func (s *stream) ID() uint32 {
return s.id
}
// Read reads data from the stream into the provided buffer.
func (s *stream) Read(b []byte) (n int, err error) {
for {
n, err = s.tryRead(b)
if err == ErrWouldBlock {
if ew := s.waitRead(); ew != nil {
return 0, ew
}
} else {
return n, err
}
}
}
// tryRead attempts to read data from the stream without blocking.
func (s *stream) tryRead(b []byte) (n int, err error) {
if s.sess.config.Version == 2 {
return s.tryReadv2(b)
}
if len(b) == 0 {
return 0, nil
}
// A critical section to copy data from buffers to
s.bufferLock.Lock()
if len(s.buffers) > 0 {
n = copy(b, s.buffers[0])
s.buffers[0] = s.buffers[0][n:]
if len(s.buffers[0]) == 0 {
s.buffers[0] = nil
s.buffers = s.buffers[1:]
// full recycle
pool.PutBuffer(s.heads[0])
s.heads = s.heads[1:]
}
}
s.bufferLock.Unlock()
if n > 0 {
s.sess.returnTokens(n)
return n, nil
}
select {
case <-s.die:
return 0, io.EOF
default:
return 0, ErrWouldBlock
}
}
// tryReadv2 is the non-blocking version of Read for version 2 streams.
func (s *stream) tryReadv2(b []byte) (n int, err error) {
if len(b) == 0 {
return 0, nil
}
var notifyConsumed uint32
s.bufferLock.Lock()
if len(s.buffers) > 0 {
n = copy(b, s.buffers[0])
s.buffers[0] = s.buffers[0][n:]
if len(s.buffers[0]) == 0 {
s.buffers[0] = nil
s.buffers = s.buffers[1:]
// full recycle
pool.PutBuffer(s.heads[0])
s.heads = s.heads[1:]
}
}
// in an ideal environment:
// if more than half of buffer has consumed, send read ack to peer
// based on round-trip time of ACK, continous flowing data
// won't slow down due to waiting for ACK, as long as the
// consumer keeps on reading data.
//
// s.numRead == n implies that it's the initial reading
s.numRead += uint32(n)
s.incr += uint32(n)
// for initial reading, send window update
if s.incr >= uint32(s.sess.config.MaxStreamBuffer/2) || s.numRead == uint32(n) {
notifyConsumed = s.numRead
s.incr = 0 // reset couting for next window update
}
s.bufferLock.Unlock()
if n > 0 {
s.sess.returnTokens(n)
// send window update if necessary
if notifyConsumed > 0 {
err := s.sendWindowUpdate(notifyConsumed)
return n, err
} else {
return n, nil
}
}
select {
case <-s.die:
return 0, io.EOF
default:
return 0, ErrWouldBlock
}
}
// WriteTo implements io.WriteTo
// WriteTo writes data to w until there's no more data to write or when an error occurs.
// The return value n is the number of bytes written. Any error encountered during the write is also returned.
// WriteTo calls Write in a loop until there is no more data to write or when an error occurs.
// If the underlying stream is a v2 stream, it will send window update to peer when necessary.
// If the underlying stream is a v1 stream, it will not send window update to peer.
func (s *stream) WriteTo(w io.Writer) (n int64, err error) {
if s.sess.config.Version == 2 {
return s.writeTov2(w)
}
for {
var buf []byte
s.bufferLock.Lock()
if len(s.buffers) > 0 {
buf = s.buffers[0]
s.buffers = s.buffers[1:]
s.heads = s.heads[1:]
}
s.bufferLock.Unlock()
if buf != nil {
nw, ew := w.Write(buf)
// NOTE: WriteTo is a reader, so we need to return tokens here
s.sess.returnTokens(len(buf))
pool.PutBuffer(buf)
if nw > 0 {
n += int64(nw)
}
if ew != nil {
return n, ew
}
} else if ew := s.waitRead(); ew != nil {
return n, ew
}
}
}
// check comments in WriteTo
func (s *stream) writeTov2(w io.Writer) (n int64, err error) {
for {
var notifyConsumed uint32
var buf []byte
s.bufferLock.Lock()
if len(s.buffers) > 0 {
buf = s.buffers[0]
s.buffers = s.buffers[1:]
s.heads = s.heads[1:]
}
s.numRead += uint32(len(buf))
s.incr += uint32(len(buf))
if s.incr >= uint32(s.sess.config.MaxStreamBuffer/2) || s.numRead == uint32(len(buf)) {
notifyConsumed = s.numRead
s.incr = 0
}
s.bufferLock.Unlock()
if buf != nil {
nw, ew := w.Write(buf)
// NOTE: WriteTo is a reader, so we need to return tokens here
s.sess.returnTokens(len(buf))
pool.PutBuffer(buf)
if nw > 0 {
n += int64(nw)
}
if ew != nil {
return n, ew
}
if notifyConsumed > 0 {
if err := s.sendWindowUpdate(notifyConsumed); err != nil {
return n, err
}
}
} else if ew := s.waitRead(); ew != nil {
return n, ew
}
}
}
// sendWindowUpdate sends a window update frame to the peer.
func (s *stream) sendWindowUpdate(consumed uint32) error {
var timer *time.Timer
var deadline <-chan time.Time
if d, ok := s.readDeadline.Load().(time.Time); ok && !d.IsZero() {
timer = time.NewTimer(time.Until(d))
defer timer.Stop()
deadline = timer.C
}
frame := newFrame(byte(s.sess.config.Version), cmdUPD, s.id)
var hdr updHeader
binary.LittleEndian.PutUint32(hdr[:], consumed)
binary.LittleEndian.PutUint32(hdr[4:], uint32(s.sess.config.MaxStreamBuffer))
frame.data = hdr[:]
_, err := s.sess.writeFrameInternal(frame, deadline, CLSCTRL)
return err
}
// waitRead blocks until a read event occurs or a deadline is reached.
func (s *stream) waitRead() error {
var timer *time.Timer
var deadline <-chan time.Time
if d, ok := s.readDeadline.Load().(time.Time); ok && !d.IsZero() {
timer = time.NewTimer(time.Until(d))
defer timer.Stop()
deadline = timer.C
}
select {
case <-s.chReadEvent: // notify some data has arrived, or closed
return nil
case <-s.chFinEvent:
// BUGFIX(xtaci): Fix for https://github.com/xtaci/smux/issues/82
s.bufferLock.Lock()
defer s.bufferLock.Unlock()
if len(s.buffers) > 0 {
return nil
}
return io.EOF
case <-s.sess.chSocketReadError:
return s.sess.socketReadError.Load().(error)
case <-s.sess.chProtoError:
return s.sess.protoError.Load().(error)
case <-deadline:
return ErrTimeout
case <-s.die:
return io.ErrClosedPipe
}
}
// Write implements net.Conn
//
// Note that the behavior when multiple goroutines write concurrently is not deterministic,
// frames may interleave in random way.
func (s *stream) Write(b []byte) (n int, err error) {
if s.sess.config.Version == 2 {
return s.writeV2(b)
}
var deadline <-chan time.Time
if d, ok := s.writeDeadline.Load().(time.Time); ok && !d.IsZero() {
timer := time.NewTimer(time.Until(d))
defer timer.Stop()
deadline = timer.C
}
// check if stream has closed
select {
case <-s.chFinEvent: // passive closing
return 0, io.EOF
case <-s.die:
return 0, io.ErrClosedPipe
default:
}
// frame split and transmit
sent := 0
frame := newFrame(byte(s.sess.config.Version), cmdPSH, s.id)
bts := b
for len(bts) > 0 {
sz := len(bts)
if sz > s.frameSize {
sz = s.frameSize
}
frame.data = bts[:sz]
bts = bts[sz:]
n, err := s.sess.writeFrameInternal(frame, deadline, CLSDATA)
s.numWritten++
sent += n
if err != nil {
return sent, err
}
}
return sent, nil
}
// writeV2 writes data to the stream for version 2 streams.
func (s *stream) writeV2(b []byte) (n int, err error) {
// check empty input
if len(b) == 0 {
return 0, nil
}
// check if stream has closed
select {
case <-s.chFinEvent:
return 0, io.EOF
case <-s.die:
return 0, io.ErrClosedPipe
default:
}
// create write deadline timer
var deadline <-chan time.Time
if d, ok := s.writeDeadline.Load().(time.Time); ok && !d.IsZero() {
timer := time.NewTimer(time.Until(d))
defer timer.Stop()
deadline = timer.C
}
// frame split and transmit process
sent := 0
frame := newFrame(byte(s.sess.config.Version), cmdPSH, s.id)
for {
// per stream sliding window control
// [.... [consumed... numWritten] ... win... ]
// [.... [consumed...................+rmtwnd]]
var bts []byte
// note:
// even if uint32 overflow, this math still works:
// eg1: uint32(0) - uint32(math.MaxUint32) = 1
// eg2: int32(uint32(0) - uint32(1)) = -1
//
// basicially, you can take it as a MODULAR ARITHMETIC
inflight := int32(atomic.LoadUint32(&s.numWritten) - atomic.LoadUint32(&s.peerConsumed))
if inflight < 0 { // security check for malformed data
return 0, ErrConsumed
}
// make sure you understand 'win' is calculated in modular arithmetic(2^32(4GB))
win := int32(atomic.LoadUint32(&s.peerWindow)) - inflight
if win > 0 {
// determine how many bytes to send
if win > int32(len(b)) {
bts = b
b = nil
} else {
bts = b[:win]
b = b[win:]
}
// frame split and transmit
for len(bts) > 0 {
// splitting frame
sz := len(bts)
if sz > s.frameSize {
sz = s.frameSize
}
frame.data = bts[:sz]
bts = bts[sz:]
// transmit of frame
n, err := s.sess.writeFrameInternal(frame, deadline, CLSDATA)
atomic.AddUint32(&s.numWritten, uint32(sz))
sent += n
if err != nil {
return sent, err
}
}
}
// if there is any data left to be sent,
// wait until stream closes, window changes or deadline reached
// this blocking behavior will back propagate flow control to upper layer.
if len(b) > 0 {
select {
case <-s.chFinEvent:
return 0, io.EOF
case <-s.die:
return sent, io.ErrClosedPipe
case <-deadline:
return sent, ErrTimeout
case <-s.sess.chSocketWriteError:
return sent, s.sess.socketWriteError.Load().(error)
case <-s.chUpdate: // notify of remote data consuming and window update
continue
}
} else {
return sent, nil
}
}
}
// Close implements net.Conn
func (s *stream) Close() error {
var once bool
var err error
s.dieOnce.Do(func() {
close(s.die)
once = true
})
if once {
// send FIN in order
f := newFrame(byte(s.sess.config.Version), cmdFIN, s.id)
timer := time.NewTimer(openCloseTimeout)
defer timer.Stop()
_, err = s.sess.writeFrameInternal(f, timer.C, CLSDATA)
s.sess.streamClosed(s.id)
return err
} else {
return io.ErrClosedPipe
}
}
// GetDieCh returns a readonly chan which can be readable
// when the stream is to be closed.
func (s *stream) GetDieCh() <-chan struct{} {
return s.die
}
// SetReadDeadline sets the read deadline as defined by
// net.Conn.SetReadDeadline.
// A zero time value disables the deadline.
func (s *stream) SetReadDeadline(t time.Time) error {
s.readDeadline.Store(t)
s.notifyReadEvent()
return nil
}
// SetWriteDeadline sets the write deadline as defined by
// net.Conn.SetWriteDeadline.
// A zero time value disables the deadline.
func (s *stream) SetWriteDeadline(t time.Time) error {
s.writeDeadline.Store(t)
return nil
}
// SetDeadline sets both read and write deadlines as defined by
// net.Conn.SetDeadline.
// A zero time value disables the deadlines.
func (s *stream) SetDeadline(t time.Time) error {
if err := s.SetReadDeadline(t); err != nil {
return err
}
if err := s.SetWriteDeadline(t); err != nil {
return err
}
return nil
}
// session closes
func (s *stream) sessionClose() { s.dieOnce.Do(func() { close(s.die) }) }
// LocalAddr satisfies net.Conn interface
func (s *stream) LocalAddr() net.Addr {
if ts, ok := s.sess.conn.(interface {
LocalAddr() net.Addr
}); ok {
return ts.LocalAddr()
}
return nil
}
// RemoteAddr satisfies net.Conn interface
func (s *stream) RemoteAddr() net.Addr {
if ts, ok := s.sess.conn.(interface {
RemoteAddr() net.Addr
}); ok {
return ts.RemoteAddr()
}
return nil
}
// pushBytes append buf to buffers
func (s *stream) pushBytes(buf []byte) (written int, err error) {
s.bufferLock.Lock()
s.buffers = append(s.buffers, buf)
s.heads = append(s.heads, buf)
s.bufferLock.Unlock()
return
}
// recycleTokens transform remaining bytes to tokens(will truncate buffer)
func (s *stream) recycleTokens() (n int) {
s.bufferLock.Lock()
for k := range s.buffers {
n += len(s.buffers[k])
pool.PutBuffer(s.heads[k])
}
s.buffers = nil
s.heads = nil
s.bufferLock.Unlock()
return
}
// notify read event
func (s *stream) notifyReadEvent() {
select {
case s.chReadEvent <- struct{}{}:
default:
}
}
// update command
func (s *stream) update(consumed uint32, window uint32) {
atomic.StoreUint32(&s.peerConsumed, consumed)
atomic.StoreUint32(&s.peerWindow, window)
select {
case s.chUpdate <- struct{}{}:
default:
}
}
// mark this stream has been closed in protocol
func (s *stream) fin() {
s.finEventOnce.Do(func() {
close(s.chFinEvent)
})
}

31
pkg/sockopt/sockopt.go Normal file
View File

@ -0,0 +1,31 @@
package sockopt
import (
"net"
"syscall"
)
// Options is the options struct.
type Options struct {
bindIface *net.Interface
reuseAddr bool
}
// Option is the function paramater.
type Option func(opts *Options)
// Bind sets the bind interface option.
func Bind(intf *net.Interface) Option { return func(opts *Options) { opts.bindIface = intf } }
// ReuseAddr sets the reuse addr option.
func ReuseAddr() Option { return func(opts *Options) { opts.reuseAddr = true } }
// Control returns a control function for the net.Dialer and net.ListenConfig.
func Control(opts ...Option) func(network, address string, c syscall.RawConn) error {
option := &Options{}
for _, opt := range opts {
opt(option)
}
return control(option)
}

View File

@ -0,0 +1,28 @@
package sockopt
import (
"syscall"
"golang.org/x/sys/unix"
)
func control(opt *Options) func(network, address string, c syscall.RawConn) error {
return func(network, address string, c syscall.RawConn) (err error) {
return c.Control(func(fd uintptr) {
if opt.bindIface != nil {
switch network {
case "tcp4", "udp4":
unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, opt.bindIface.Index)
case "tcp6", "udp6":
unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, opt.bindIface.Index)
}
}
if opt.reuseAddr {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
}
})
}
}

View File

@ -0,0 +1,23 @@
package sockopt
import (
"syscall"
"golang.org/x/sys/unix"
)
func control(opt *Options) func(network, address string, c syscall.RawConn) error {
return func(network, address string, c syscall.RawConn) (err error) {
return c.Control(func(fd uintptr) {
if opt.bindIface != nil {
unix.BindToDevice(int(fd), opt.bindIface.Name)
}
if opt.reuseAddr {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
}
})
}
}

View File

@ -0,0 +1,9 @@
//go:build !linux && !darwin
package sockopt
import (
"syscall"
)
func control(opt *Options) func(string, string, syscall.RawConn) error { return nil }

View File

@ -4,6 +4,7 @@ import (
"errors"
"io"
"net"
"net/netip"
"strconv"
)
@ -13,21 +14,22 @@ const (
AuthPassword = 2
)
// SOCKS request commands as defined in RFC 1928 section 4.
// SOCKS request commands as defined in RFC 1928 section 4
const (
CmdConnect = 1
CmdBind = 2
CmdUDPAssociate = 3
CmdError byte = 0
CmdConnect byte = 1
CmdBind byte = 2
CmdUDPAssociate byte = 3
)
// SOCKS address types as defined in RFC 1928 section 5.
// SOCKS address types as defined in RFC 1928 section 5
const (
ATypIP4 = 1
ATypDomain = 3
ATypIP6 = 4
)
// MaxAddrLen is the maximum size of SOCKS address in bytes.
// MaxAddrLen is the maximum size of SOCKS address in bytes
const MaxAddrLen = 1 + 1 + 255 + 2
// Errors are socks5 errors
@ -44,14 +46,14 @@ var Errors = []error{
errors.New("socks5UDPAssociate"),
}
// Addr .
// Addr represents a SOCKS address as defined in RFC 1928 section 5.
type Addr []byte
// String serializes SOCKS address a to string form.
func (a Addr) String() string {
var host, port string
switch ATYP(a[0]) { // address type
switch a[0] { // address type
case ATypDomain:
host = string(a[2 : 2+int(a[1])])
port = strconv.Itoa((int(a[2+int(a[1])]) << 8) | int(a[2+int(a[1])+1]))
@ -66,27 +68,18 @@ func (a Addr) String() string {
return net.JoinHostPort(host, port)
}
// UoT returns whether it is udp over tcp
func UoT(b byte) bool {
return b&0x8 == 0x8
}
// Network returns network name. Implements net.Addr interface.
func (a Addr) Network() string { return "socks" }
// ATYP returns the address type
func ATYP(b byte) int {
return int(b &^ 0x8)
}
// ReadAddrBuf reads just enough bytes from r to get a valid Addr.
func ReadAddrBuf(r io.Reader, b []byte) (Addr, error) {
if len(b) < MaxAddrLen {
return nil, io.ErrShortBuffer
}
// ReadAddr reads just enough bytes from r to get a valid Addr.
func ReadAddr(r io.Reader) (Addr, error) {
b := make([]byte, MaxAddrLen)
_, err := io.ReadFull(r, b[:1]) // read 1st byte for address type
if err != nil {
return nil, err
}
switch ATYP(b[0]) {
switch b[0] {
case ATypDomain:
_, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length
if err != nil {
@ -105,11 +98,6 @@ func ReadAddrBuf(r io.Reader, b []byte) (Addr, error) {
return nil, Errors[8]
}
// ReadAddr reads just enough bytes from r to get a valid Addr.
func ReadAddr(r io.Reader) (Addr, error) {
return ReadAddrBuf(r, make([]byte, MaxAddrLen))
}
// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed.
func SplitAddr(b []byte) Addr {
addrLen := 1
@ -117,7 +105,7 @@ func SplitAddr(b []byte) Addr {
return nil
}
switch ATYP(b[0]) {
switch b[0] {
case ATypDomain:
if len(b) < 2 {
return nil
@ -129,7 +117,6 @@ func SplitAddr(b []byte) Addr {
addrLen = 1 + net.IPv6len + 2
default:
return nil
}
if len(b) < addrLen {
@ -146,16 +133,16 @@ func ParseAddr(s string) Addr {
if err != nil {
return nil
}
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
if ip, err := netip.ParseAddr(host); err == nil {
if ip.Is4() {
addr = make([]byte, 1+net.IPv4len+2)
addr[0] = ATypIP4
copy(addr[1:], ip4)
} else {
addr = make([]byte, 1+net.IPv6len+2)
addr[0] = ATypIP6
copy(addr[1:], ip)
}
copy(addr[1:], ip.AsSlice())
} else {
if len(host) > 255 {
return nil

206
proxy/conn.go Normal file
View File

@ -0,0 +1,206 @@
package proxy
import (
"bufio"
"errors"
"io"
"net"
"os"
"runtime"
"sync"
"time"
"github.com/nadoo/glider/pkg/pool"
)
var (
// TCPBufSize is the size of tcp buffer.
TCPBufSize = 32 << 10
// UDPBufSize is the size of udp buffer.
UDPBufSize = 2 << 10
)
// Conn is a connection with buffered reader.
type Conn struct {
r *bufio.Reader
net.Conn
}
// NewConn returns a new conn.
func NewConn(c net.Conn) *Conn {
if conn, ok := c.(*Conn); ok {
return conn
}
return &Conn{pool.GetBufReader(c), c}
}
// Reader returns the internal bufio.Reader.
func (c *Conn) Reader() *bufio.Reader { return c.r }
func (c *Conn) Read(p []byte) (int, error) { return c.r.Read(p) }
// Peek returns the next n bytes without advancing the reader.
func (c *Conn) Peek(n int) ([]byte, error) { return c.r.Peek(n) }
// WriteTo implements io.WriterTo.
func (c *Conn) WriteTo(w io.Writer) (n int64, err error) { return c.r.WriteTo(w) }
// Close closes the Conn.
func (c *Conn) Close() error {
pool.PutBufReader(c.r)
return c.Conn.Close()
}
// Relay relays between left and right.
func Relay(left, right net.Conn) error {
var err, err1 error
var wg sync.WaitGroup
var wait = 5 * time.Second
wg.Add(1)
go func() {
defer wg.Done()
_, err1 = Copy(right, left)
right.SetReadDeadline(time.Now().Add(wait)) // unblock read on right
}()
_, err = Copy(left, right)
left.SetReadDeadline(time.Now().Add(wait)) // unblock read on left
wg.Wait()
if err1 != nil && !errors.Is(err1, os.ErrDeadlineExceeded) {
return err1
}
if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) {
return err
}
return nil
}
// Copy copies from src to dst.
func Copy(dst io.Writer, src io.Reader) (written int64, err error) {
dst = underlyingWriter(dst)
switch runtime.GOOS {
case "linux", "windows", "dragonfly", "freebsd", "solaris":
if _, ok := dst.(*net.TCPConn); ok && worthTry(src) {
if wt, ok := src.(io.WriterTo); ok {
return wt.WriteTo(dst)
}
if rt, ok := dst.(io.ReaderFrom); ok {
return rt.ReadFrom(src)
}
}
}
return CopyBuffer(dst, src)
}
func underlyingWriter(c io.Writer) io.Writer {
if wrap, ok := c.(*Conn); ok {
return wrap.Conn
}
return c
}
func worthTry(src io.Reader) bool {
switch v := src.(type) {
case *net.TCPConn, *net.UnixConn:
return true
case *io.LimitedReader:
return worthTry(v.R)
case *Conn:
return worthTry(v.Conn)
case *os.File:
fi, err := v.Stat()
if err != nil {
return false
}
return fi.Mode().IsRegular()
default:
return false
}
}
// CopyN copies n bytes (or until an error) from src to dst.
func CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
written, err = Copy(dst, io.LimitReader(src, n))
if written == n {
return n, nil
}
if written < n && err == nil {
// src stopped early; must have been EOF.
err = io.EOF
}
return
}
// CopyBuffer copies from src to dst with a userspace buffer.
func CopyBuffer(dst io.Writer, src io.Reader) (written int64, err error) {
size := TCPBufSize
if l, ok := src.(*io.LimitedReader); ok && int64(size) > l.N {
if l.N < 1 {
size = 1
} else {
size = int(l.N)
}
}
buf := pool.GetBuffer(size)
defer pool.PutBuffer(buf)
for {
nr, er := src.Read(buf)
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
if nw > 0 {
written += int64(nw)
}
if ew != nil {
err = ew
break
}
if nr != nw {
err = io.ErrShortWrite
break
}
}
if er != nil {
if er != io.EOF {
err = er
}
break
}
}
return written, err
}
// CopyUDP copys from src to dst at target with read timeout.
// if step sets to non-zero value,
// the read timeout will be increased from 0 to timeout by step in every read operation.
func CopyUDP(dst net.PacketConn, writeTo net.Addr, src net.PacketConn, timeout time.Duration, step time.Duration) error {
buf := pool.GetBuffer(UDPBufSize)
defer pool.PutBuffer(buf)
var t time.Duration
for {
if t += step; t == 0 || t > timeout {
t = timeout
}
src.SetReadDeadline(time.Now().Add(t))
n, addr, err := src.ReadFrom(buf)
if err != nil {
return err
}
if writeTo != nil {
addr = writeTo
}
_, err = dst.WriteTo(buf[:n], addr)
if err != nil {
return err
}
}
}

View File

@ -3,55 +3,77 @@ package proxy
import (
"errors"
"net"
"net/url"
"sort"
"strings"
"github.com/nadoo/glider/common/log"
)
// Dialer means to establish a connection and relay it.
var (
// ErrNotSupported indicates that the operation is not supported
ErrNotSupported = errors.New("not supported")
)
// Dialer is used to create connection.
type Dialer interface {
// Addr()
TCPDialer
UDPDialer
}
// TCPDialer is used to create tcp connection.
type TCPDialer interface {
// Addr is the dialer's addr
Addr() string
// Dial connects to the given address via the proxy.
// Dial connects to the given address
Dial(network, addr string) (c net.Conn, err error)
}
// DialUDP connects to the given address via the proxy.
DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error)
// UDPDialer is used to create udp PacketConn.
type UDPDialer interface {
// Addr is the dialer's addr
Addr() string
// Get the dialer by dstAddr
NextDialer(dstAddr string) Dialer
// DialUDP connects to the given address
DialUDP(network, addr string) (pc net.PacketConn, err error)
}
// DialerCreator is a function to create dialers.
type DialerCreator func(s string, dialer Dialer) (Dialer, error)
var (
dialerMap = make(map[string]DialerCreator)
dialerCreators = make(map[string]DialerCreator)
)
// RegisterDialer is used to register a dialer
// RegisterDialer is used to register a dialer.
func RegisterDialer(name string, c DialerCreator) {
dialerMap[name] = c
dialerCreators[strings.ToLower(name)] = c
}
// DialerFromURL calls the registered creator to create dialers.
// dialer is the default upstream dialer so cannot be nil, we can use Default when calling this function.
func DialerFromURL(s string, dialer Dialer) (Dialer, error) {
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
return nil, err
}
if dialer == nil {
dialer = Direct
return nil, errors.New("DialerFromURL: dialer cannot be nil")
}
c, ok := dialerMap[strings.ToLower(u.Scheme)]
if !strings.Contains(s, "://") {
s = s + "://"
}
scheme := s[:strings.Index(s, ":")]
c, ok := dialerCreators[strings.ToLower(scheme)]
if ok {
return c(s, dialer)
}
return nil, errors.New("unknown scheme '" + u.Scheme + "'")
return nil, errors.New("unknown scheme '" + scheme + "'")
}
// DialerSchemes returns the registered dialer schemes.
func DialerSchemes() string {
s := make([]string, 0, len(dialerCreators))
for name := range dialerCreators {
s = append(s, name)
}
sort.Strings(s)
return strings.Join(s, " ")
}

View File

@ -1,25 +1,97 @@
package proxy
import (
"context"
"errors"
"net"
"net/netip"
"time"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/pkg/sockopt"
)
// direct proxy
type direct struct{}
// Direct proxy
var Direct = &direct{}
func (d *direct) Addr() string { return "DIRECT" }
func (d *direct) Dial(network, addr string) (net.Conn, error) {
if network == "uot" {
network = "udp"
// Direct proxy.
type Direct struct {
iface *net.Interface // interface specified by user
ip net.IP
dialTimeout time.Duration
relayTimeout time.Duration
}
c, err := net.Dial(network, addr)
func init() {
RegisterDialer("direct", NewDirectDialer)
}
// NewDirect returns a Direct dialer.
func NewDirect(intface string, dialTimeout, relayTimeout time.Duration) (*Direct, error) {
d := &Direct{dialTimeout: dialTimeout, relayTimeout: relayTimeout}
if intface != "" {
if addr, err := netip.ParseAddr(intface); err == nil {
d.ip = addr.AsSlice()
} else {
iface, err := net.InterfaceByName(intface)
if err != nil {
return nil, errors.New(err.Error() + ": " + intface)
}
d.iface = iface
}
}
return d, nil
}
// NewDirectDialer returns a direct dialer.
func NewDirectDialer(s string, d Dialer) (Dialer, error) {
if d == nil {
return NewDirect("", time.Duration(3)*time.Second, time.Duration(3)*time.Second)
}
return d, nil
}
// Addr returns forwarder's address.
func (d *Direct) Addr() string { return "DIRECT" }
// Dial connects to the address addr on the network net
func (d *Direct) Dial(network, addr string) (c net.Conn, err error) {
if d.iface == nil || d.ip != nil {
c, err = d.dial(network, addr, d.ip)
if err == nil {
return
}
}
for _, ip := range d.IFaceIPs() {
c, err = d.dial(network, addr, ip)
if err == nil {
d.ip = ip
break
}
}
// no ip available (so no dials made), maybe the interface link is down
if c == nil && err == nil {
err = errors.New("dial failed, maybe the interface link is down, please check it")
}
return c, err
}
func (d *Direct) dial(network, addr string, localIP net.IP) (net.Conn, error) {
var la net.Addr
switch network {
case "tcp":
la = &net.TCPAddr{IP: localIP}
case "udp":
la = &net.UDPAddr{IP: localIP}
}
dialer := &net.Dialer{LocalAddr: la, Timeout: d.dialTimeout}
if d.iface != nil {
dialer.Control = sockopt.Control(sockopt.Bind(d.iface))
}
c, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
}
@ -28,19 +100,52 @@ func (d *direct) Dial(network, addr string) (net.Conn, error) {
c.SetKeepAlive(true)
}
if d.relayTimeout > 0 {
c.SetDeadline(time.Now().Add(d.relayTimeout))
}
return c, err
}
// DialUDP connects to the given address via the proxy
func (d *direct) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) {
pc, err := net.ListenPacket(network, "")
// DialUDP connects to the given address.
func (d *Direct) DialUDP(network, addr string) (net.PacketConn, error) {
var la string
if d.ip != nil {
la = net.JoinHostPort(d.ip.String(), "0")
}
lc := &net.ListenConfig{}
if d.iface != nil {
lc.Control = sockopt.Control(sockopt.Bind(d.iface))
}
return lc.ListenPacket(context.Background(), network, la)
}
// IFaceIPs returns ip addresses according to the specified interface.
func (d *Direct) IFaceIPs() (ips []net.IP) {
ipNets, err := d.iface.Addrs()
if err != nil {
log.F("ListenPacket error: %s", err)
return nil, nil, err
return
}
for _, ipNet := range ipNets {
ips = append(ips, ipNet.(*net.IPNet).IP) //!ip.IsLinkLocalUnicast()
}
return
}
uAddr, err := net.ResolveUDPAddr("udp", addr)
return pc, uAddr, err
}
func init() {
AddUsage("direct", `
Direct scheme:
direct://
func (d *direct) NextDialer(dstAddr string) Dialer { return d }
Only needed when you want to specify the outgoing interface:
glider -verbose -listen :8443 -forward direct://#interface=eth0
Or load balance multiple interfaces directly:
glider -verbose -listen :8443 -forward direct://#interface=eth0 -forward direct://#interface=eth1 -strategy rr
Or you can use the high availability mode:
glider -verbose -listen :8443 -forward direct://#interface=eth0&priority=100 -forward direct://#interface=eth1&priority=200 -strategy ha
`)
}

View File

@ -1,64 +0,0 @@
// https://tools.ietf.org/html/rfc1035
package dnstun
import (
"net/url"
"strings"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/dns"
"github.com/nadoo/glider/proxy"
"github.com/nadoo/glider/proxy/tcptun"
)
// DNSTun struct
type DNSTun struct {
dialer proxy.Dialer
addr string
raddr string
dns *dns.DNS
tcp *tcptun.TCPTun
}
func init() {
proxy.RegisterServer("dnstun", NewDNSTunServer)
}
// NewDNSTun returns a dns tunnel forwarder.
func NewDNSTun(s string, dialer proxy.Dialer) (*DNSTun, error) {
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
return nil, err
}
addr := u.Host
d := strings.Split(addr, "=")
addr, raddr := d[0], d[1]
p := &DNSTun{
dialer: dialer,
addr: addr,
raddr: raddr,
}
p.dns, _ = dns.NewDNS(addr, raddr, dialer, true)
return p, nil
}
// NewDNSTunServer returns a dns tunnel server.
func NewDNSTunServer(s string, dialer proxy.Dialer) (proxy.Server, error) {
return NewDNSTun(s, dialer)
}
// ListenAndServe .
func (s *DNSTun) ListenAndServe() {
if s.dns != nil {
go s.dns.ListenAndServe()
}
}

82
proxy/http/client.go Normal file
View File

@ -0,0 +1,82 @@
package http
import (
"encoding/base64"
"errors"
"net"
"net/textproto"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/proxy"
)
// NewHTTPDialer returns a http proxy dialer.
func NewHTTPDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
return NewHTTP(s, d, nil)
}
// Addr returns forwarder's address.
func (s *HTTP) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// Dial connects to the address addr on the network net via the proxy.
func (s *HTTP) Dial(network, addr string) (net.Conn, error) {
rc, err := s.dialer.Dial(network, s.addr)
if err != nil {
log.F("[http] dial to %s error: %s", s.addr, err)
return nil, err
}
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
buf.WriteString("CONNECT " + addr + " HTTP/1.1\r\n")
buf.WriteString("Host: " + addr + "\r\n")
buf.WriteString("Proxy-Connection: Keep-Alive\r\n")
if s.user != "" && s.password != "" {
auth := s.user + ":" + s.password
buf.WriteString("Proxy-Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + "\r\n")
}
// header ended
buf.WriteString("\r\n")
_, err = rc.Write(buf.Bytes())
if err != nil {
return nil, err
}
c := proxy.NewConn(rc)
tpr := textproto.NewReader(c.Reader())
line, err := tpr.ReadLine()
if err != nil {
return c, err
}
_, code, _, ok := parseStartLine(line)
if ok && code == "200" {
tpr.ReadMIMEHeader()
return c, err
}
switch code {
case "403":
log.F("[http] 'CONNECT' to ports other than 443 are not allowed by proxy %s", s.addr)
case "405":
log.F("[http] 'CONNECT' method not allowed by proxy %s", s.addr)
case "407":
log.F("[http] authencation needed by proxy %s", s.addr)
}
return nil, errors.New("[http] can not connect remote address: " + addr + ". error code: " + code)
}
// DialUDP connects to the given address via the proxy.
func (s *HTTP) DialUDP(network, addr string) (pc net.PacketConn, err error) {
return nil, proxy.ErrNotSupported
}

View File

@ -1,32 +1,28 @@
// http proxy
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
// NOTE: never keep-alive so the implementation can be much easier.
// Package http implements a http proxy.
package http
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/textproto"
"net/url"
"strings"
"time"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
)
// HTTP struct
// HTTP struct.
type HTTP struct {
dialer proxy.Dialer
proxy proxy.Proxy
addr string
user string
password string
pretend bool
}
func init() {
@ -35,7 +31,7 @@ func init() {
}
// NewHTTP returns a http proxy.
func NewHTTP(s string, dialer proxy.Dialer) (*HTTP, error) {
func NewHTTP(s string, d proxy.Dialer, p proxy.Proxy) (*HTTP, error) {
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
@ -47,229 +43,23 @@ func NewHTTP(s string, dialer proxy.Dialer) (*HTTP, error) {
pass, _ := u.User.Password()
h := &HTTP{
dialer: dialer,
dialer: d,
proxy: p,
addr: addr,
user: user,
password: pass,
pretend: false,
}
if u.Query().Get("pretend") == "true" {
h.pretend = true
}
return h, nil
}
// NewHTTPDialer returns a http proxy dialer.
func NewHTTPDialer(s string, dialer proxy.Dialer) (proxy.Dialer, error) {
return NewHTTP(s, dialer)
}
// NewHTTPServer returns a http proxy server.
func NewHTTPServer(s string, dialer proxy.Dialer) (proxy.Server, error) {
return NewHTTP(s, dialer)
}
// ListenAndServe .
func (s *HTTP) ListenAndServe() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.F("failed to listen on %s: %v", s.addr, err)
return
}
defer l.Close()
log.F("listening TCP on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[http] failed to accept: %v", err)
continue
}
go s.Serve(c)
}
}
// Serve .
func (s *HTTP) Serve(c net.Conn) {
defer c.Close()
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
reqR := bufio.NewReader(c)
reqTP := textproto.NewReader(reqR)
method, requestURI, proto, ok := parseFirstLine(reqTP)
if !ok {
return
}
if method == "CONNECT" {
s.servHTTPS(method, requestURI, proto, c)
return
}
reqHeader, err := reqTP.ReadMIMEHeader()
if err != nil {
log.F("read header error:%s", err)
return
}
cleanHeaders(reqHeader)
// tell the remote server not to keep alive
reqHeader.Set("Connection", "close")
url, err := url.ParseRequestURI(requestURI)
if err != nil {
log.F("[http] parse request url error: %s", err)
return
}
var tgt = url.Host
if !strings.Contains(url.Host, ":") {
tgt += ":80"
}
rc, err := s.dialer.Dial("tcp", tgt)
if err != nil {
fmt.Fprintf(c, "%s 502 ERROR\r\n\r\n", proto)
log.F("[http] failed to dial: %v", err)
return
}
defer rc.Close()
// GET http://example.com/a/index.htm HTTP/1.1 -->
// GET /a/index.htm HTTP/1.1
url.Scheme = ""
url.Host = ""
uri := url.String()
var reqBuf bytes.Buffer
writeFirstLine(method, uri, proto, &reqBuf)
writeHeaders(reqHeader, &reqBuf)
// send request to remote server
rc.Write(reqBuf.Bytes())
// copy the left request bytes to remote server. eg. length specificed or chunked body.
go func() {
if _, err := reqR.Peek(1); err == nil {
io.Copy(rc, reqR)
rc.SetDeadline(time.Now())
c.SetDeadline(time.Now())
}
}()
respR := bufio.NewReader(rc)
respTP := textproto.NewReader(respR)
proto, code, status, ok := parseFirstLine(respTP)
if !ok {
return
}
respHeader, err := respTP.ReadMIMEHeader()
if err != nil {
log.F("[http] read header error:%s", err)
return
}
respHeader.Set("Proxy-Connection", "close")
respHeader.Set("Connection", "close")
var respBuf bytes.Buffer
writeFirstLine(proto, code, status, &respBuf)
writeHeaders(respHeader, &respBuf)
log.F("[http] %s <-> %s", c.RemoteAddr(), tgt)
c.Write(respBuf.Bytes())
io.Copy(c, respR)
}
func (s *HTTP) servHTTPS(method, requestURI, proto string, c net.Conn) {
rc, err := s.dialer.Dial("tcp", requestURI)
if err != nil {
c.Write([]byte(proto))
c.Write([]byte(" 502 ERROR\r\n\r\n"))
log.F("[http] failed to dial: %v", err)
return
}
c.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n"))
log.F("[http] %s <-> %s [c]", c.RemoteAddr(), requestURI)
_, _, err = conn.Relay(c, rc)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
return // ignore i/o timeout
}
log.F("relay error: %v", err)
}
}
// Addr returns forwarder's address
func (s *HTTP) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// NextDialer returns the next dialer
func (s *HTTP) NextDialer(dstAddr string) proxy.Dialer { return s.dialer.NextDialer(dstAddr) }
// Dial connects to the address addr on the network net via the proxy.
func (s *HTTP) Dial(network, addr string) (net.Conn, error) {
rc, err := s.dialer.Dial(network, s.addr)
if err != nil {
log.F("[http] dial to %s error: %s", s.addr, err)
return nil, err
}
if c, ok := rc.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
rc.Write([]byte("CONNECT " + addr + " HTTP/1.0\r\n"))
rc.Write([]byte("Proxy-Connection: close\r\n"))
if s.user != "" && s.password != "" {
auth := s.user + ":" + s.password
rc.Write([]byte("Proxy-Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + "\r\n"))
}
//header ended
rc.Write([]byte("\r\n"))
respR := bufio.NewReader(rc)
respTP := textproto.NewReader(respR)
_, code, _, ok := parseFirstLine(respTP)
if ok && code == "200" {
return rc, err
} else if code == "407" {
log.F("[http] authencation needed by proxy %s", s.addr)
} else if code == "405" {
log.F("[http] 'CONNECT' method not allowed by proxy %s", s.addr)
}
return nil, errors.New("[http] can not connect remote address: " + addr + ". error code: " + code)
}
// DialUDP connects to the given address via the proxy.
func (s *HTTP) DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error) {
return nil, nil, errors.New("http client does not support udp")
}
// parseFirstLine parses "GET /foo HTTP/1.1" OR "HTTP/1.1 200 OK" into its three parts.
func parseFirstLine(tp *textproto.Reader) (r1, r2, r3 string, ok bool) {
line, err := tp.ReadLine()
// log.F("first line: %s", line)
if err != nil {
log.F("[http] read first line error:%s", err)
return
}
// parseStartLine parses "GET /foo HTTP/1.1" OR "HTTP/1.1 200 OK" into its three parts.
func parseStartLine(line string) (r1, r2, r3 string, ok bool) {
s1 := strings.Index(line, " ")
s2 := strings.Index(line[s1+1:], " ")
if s1 < 0 || s2 < 0 {
@ -291,28 +81,41 @@ func cleanHeaders(header textproto.MIMEHeader) {
header.Del("Upgrade")
}
func writeFirstLine(s1, s2, s3 string, buf *bytes.Buffer) {
buf.Write([]byte(s1))
buf.Write([]byte(" "))
buf.Write([]byte(s2))
buf.Write([]byte(" "))
buf.Write([]byte(s3))
buf.Write([]byte("\r\n"))
func writeStartLine(w io.Writer, s1, s2, s3 string) {
io.WriteString(w, s1+" "+s2+" "+s3+"\r\n")
}
func writeHeaders(header textproto.MIMEHeader, buf *bytes.Buffer) {
func writeHeaders(w io.Writer, header textproto.MIMEHeader) {
for key, values := range header {
buf.Write([]byte(key))
buf.Write([]byte(": "))
for k, v := range values {
buf.Write([]byte(v))
if k > 0 {
buf.Write([]byte(" "))
for _, v := range values {
io.WriteString(w, key+": "+v+"\r\n")
}
}
buf.Write([]byte("\r\n"))
io.WriteString(w, "\r\n")
}
//header ended
buf.Write([]byte("\r\n"))
func extractUserPass(auth string) (username, password string, ok bool) {
if !strings.HasPrefix(auth, "Basic ") {
return
}
b, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic "))
if err != nil {
return
}
s := string(b)
idx := strings.IndexByte(s, ':')
if idx < 0 {
return
}
return s[:idx], s[idx+1:], true
}
func init() {
proxy.AddUsage("http", `
Http scheme:
http://[user:pass@]host:port
`)
}

120
proxy/http/request.go Normal file
View File

@ -0,0 +1,120 @@
package http
import (
"bufio"
"bytes"
"fmt"
"net/textproto"
"net/url"
"strings"
"github.com/nadoo/glider/pkg/log"
)
// Methods are http methods from rfc.
// https://www.rfc-editor.org/rfc/rfc2616, http methods must be uppercase
var Methods = [...][]byte{
[]byte("GET"),
[]byte("POST"),
[]byte("PUT"),
[]byte("DELETE"),
[]byte("CONNECT"),
[]byte("HEAD"),
[]byte("OPTIONS"),
[]byte("TRACE"),
[]byte("PATCH"),
}
type request struct {
method string
uri string
proto string
auth string
header textproto.MIMEHeader
target string // target host with port
ruri string // relative uri
absuri string // absolute uri
}
func parseRequest(r *bufio.Reader) (*request, error) {
tpr := textproto.NewReader(r)
line, err := tpr.ReadLine()
if err != nil {
return nil, err
}
method, uri, proto, ok := parseStartLine(line)
if !ok {
return nil, fmt.Errorf("error in parseStartLine: %s", line)
}
header, err := tpr.ReadMIMEHeader()
if err != nil {
log.F("[http] read header error:%s", err)
return nil, err
}
auth := header.Get("Proxy-Authorization")
cleanHeaders(header)
header.Set("Connection", "close")
// https://github.com/golang/go/blob/dcf0929de6a12103a8fd7097abd6e797188c366d/src/net/http/request.go#L1047
justAuthority := method == "CONNECT" && !strings.HasPrefix(uri, "/")
if justAuthority {
uri = "http://" + uri
}
u, err := url.ParseRequestURI(uri)
if err != nil {
log.F("[http] parse request url error: %s, uri: %s", err, uri)
return nil, err
}
if justAuthority {
// Strip the bogus "http://" back off.
u.Scheme = ""
uri = uri[7:]
}
tgt := u.Host
if !strings.Contains(tgt, ":") {
tgt += ":80"
}
req := &request{
method: method,
uri: uri,
proto: proto,
auth: auth,
header: header,
target: tgt,
}
if u.IsAbs() {
req.absuri = u.String()
u.Scheme, u.Host = "", ""
req.ruri = u.String()
} else {
req.ruri = u.String()
base, err := url.Parse("http://" + header.Get("Host"))
if err != nil {
return nil, err
}
u = base.ResolveReference(u)
req.absuri = u.String()
}
return req, nil
}
func (r *request) WriteBuf(buf *bytes.Buffer) {
writeStartLine(buf, r.method, r.ruri, r.proto)
writeHeaders(buf, r.header)
}
func (r *request) WriteAbsBuf(buf *bytes.Buffer) {
writeStartLine(buf, r.method, r.absuri, r.proto)
writeHeaders(buf, r.header)
}

166
proxy/http/server.go Normal file
View File

@ -0,0 +1,166 @@
package http
import (
"fmt"
"io"
"net"
"net/textproto"
"strings"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/proxy"
)
// NewHTTPServer returns a http proxy server.
func NewHTTPServer(s string, p proxy.Proxy) (proxy.Server, error) {
return NewHTTP(s, nil, p)
}
// ListenAndServe listens on server's addr and serves connections.
func (s *HTTP) ListenAndServe() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.Fatalf("[http] failed to listen on %s: %v", s.addr, err)
return
}
defer l.Close()
log.F("[http] listening TCP on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[http] failed to accept: %v", err)
continue
}
go s.Serve(c)
}
}
// Serve serves a connection.
func (s *HTTP) Serve(cc net.Conn) {
if c, ok := cc.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
c := proxy.NewConn(cc)
defer c.Close()
req, err := parseRequest(c.Reader())
if err != nil {
log.F("[http] can not parse request from %s, error: %v", c.RemoteAddr(), err)
return
}
if s.pretend {
fmt.Fprintf(c, "%s 404 Not Found\r\nServer: nginx\r\n\r\n404 Not Found\r\n", req.proto)
log.F("[http] %s <-> %s, pretend as web server", c.RemoteAddr().String(), s.Addr())
return
}
s.servRequest(req, c)
}
func (s *HTTP) servRequest(req *request, c *proxy.Conn) {
// Auth
if s.user != "" && s.password != "" {
if user, pass, ok := extractUserPass(req.auth); !ok || user != s.user || pass != s.password {
io.WriteString(c, "HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic\r\n\r\n")
log.F("[http] auth failed from %s, auth info: %s:%s", c.RemoteAddr(), user, pass)
return
}
}
if req.method == "CONNECT" {
s.servHTTPS(req, c)
return
}
s.servHTTP(req, c)
}
func (s *HTTP) servHTTPS(r *request, c net.Conn) {
rc, dialer, err := s.proxy.Dial("tcp", r.uri)
if err != nil {
io.WriteString(c, r.proto+" 502 ERROR\r\n\r\n")
log.F("[http] %s <-> %s [c] via %s, error in dial: %v", c.RemoteAddr(), r.uri, dialer.Addr(), err)
return
}
defer rc.Close()
io.WriteString(c, "HTTP/1.1 200 Connection established\r\n\r\n")
log.F("[http] %s <-> %s [c] via %s", c.RemoteAddr(), r.uri, dialer.Addr())
if err = proxy.Relay(c, rc); err != nil {
log.F("[http] %s <-> %s via %s, relay error: %v", c.RemoteAddr(), r.uri, dialer.Addr(), err)
// record remote conn failure only
if !strings.Contains(err.Error(), s.addr) {
s.proxy.Record(dialer, false)
}
}
}
func (s *HTTP) servHTTP(req *request, c *proxy.Conn) {
rc, dialer, err := s.proxy.Dial("tcp", req.target)
if err != nil {
fmt.Fprintf(c, "%s 502 ERROR\r\n\r\n", req.proto)
log.F("[http] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), req.target, dialer.Addr(), err)
return
}
defer rc.Close()
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
// send request to remote server
req.WriteBuf(buf)
_, err = rc.Write(buf.Bytes())
if err != nil {
return
}
// copy the left request bytes to remote server. eg. length specificed or chunked body.
go func() {
if _, err := c.Reader().Peek(1); err == nil {
proxy.Copy(rc, c)
rc.SetDeadline(time.Now())
c.SetDeadline(time.Now())
}
}()
r := pool.GetBufReader(rc)
defer pool.PutBufReader(r)
tpr := textproto.NewReader(r)
line, err := tpr.ReadLine()
if err != nil {
return
}
proto, code, status, ok := parseStartLine(line)
if !ok {
return
}
header, err := tpr.ReadMIMEHeader()
if err != nil {
log.F("[http] read header error:%s", err)
return
}
header.Set("Proxy-Connection", "close")
header.Set("Connection", "close")
buf.Reset()
writeStartLine(buf, proto, code, status)
writeHeaders(buf, header)
log.F("[http] %s <-> %s via %s", c.RemoteAddr(), req.target, dialer.Addr())
c.Write(buf.Bytes())
proxy.Copy(c, r)
}

281
proxy/kcp/kcp.go Normal file
View File

@ -0,0 +1,281 @@
package kcp
import (
"crypto/sha1"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"strings"
kcp "github.com/xtaci/kcp-go/v5"
"golang.org/x/crypto/pbkdf2"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
)
// KCP struct.
type KCP struct {
dialer proxy.Dialer
proxy proxy.Proxy
addr string
key string
crypt string
block kcp.BlockCrypt
mode string
dataShards int
parityShards int
server proxy.Server
}
func init() {
proxy.RegisterDialer("kcp", NewKCPDialer)
proxy.RegisterServer("kcp", NewKCPServer)
}
// NewKCP returns a kcp proxy struct.
func NewKCP(s string, d proxy.Dialer, p proxy.Proxy) (*KCP, error) {
u, err := url.Parse(s)
if err != nil {
log.F("[kcp] parse url err: %s", err)
return nil, err
}
addr := u.Host
crypt := u.User.Username()
key, _ := u.User.Password()
query := u.Query()
// dataShards
dShards := query.Get("dataShards")
if dShards == "" {
dShards = "10"
}
dataShards, err := strconv.ParseUint(dShards, 10, 32)
if err != nil {
log.F("[kcp] parse dataShards err: %s", err)
return nil, err
}
// parityShards
pShards := query.Get("parityShards")
if pShards == "" {
pShards = "3"
}
parityShards, err := strconv.ParseUint(pShards, 10, 32)
if err != nil {
log.F("[kcp] parse parityShards err: %s", err)
return nil, err
}
k := &KCP{
dialer: d,
proxy: p,
addr: addr,
key: key,
crypt: crypt,
mode: query.Get("mode"),
dataShards: int(dataShards),
parityShards: int(parityShards),
}
if k.crypt != "" {
k.block, err = block(k.crypt, k.key)
if err != nil {
return nil, fmt.Errorf("[kcp] error: %s", err)
}
}
if k.mode == "" {
k.mode = "fast"
}
return k, nil
}
func block(crypt, key string) (block kcp.BlockCrypt, err error) {
pass := pbkdf2.Key([]byte(key), []byte("kcp-go"), 4096, 32, sha1.New)
switch crypt {
case "sm4":
block, _ = kcp.NewSM4BlockCrypt(pass[:16])
case "tea":
block, _ = kcp.NewTEABlockCrypt(pass[:16])
case "xor":
block, _ = kcp.NewSimpleXORBlockCrypt(pass)
case "none":
block, _ = kcp.NewNoneBlockCrypt(pass)
case "aes":
block, _ = kcp.NewAESBlockCrypt(pass)
case "aes-128":
block, _ = kcp.NewAESBlockCrypt(pass[:16])
case "aes-192":
block, _ = kcp.NewAESBlockCrypt(pass[:24])
case "blowfish":
block, _ = kcp.NewBlowfishBlockCrypt(pass)
case "twofish":
block, _ = kcp.NewTwofishBlockCrypt(pass)
case "cast5":
block, _ = kcp.NewCast5BlockCrypt(pass[:16])
case "3des":
block, _ = kcp.NewTripleDESBlockCrypt(pass[:24])
case "xtea":
block, _ = kcp.NewXTEABlockCrypt(pass[:16])
case "salsa20":
block, _ = kcp.NewSalsa20BlockCrypt(pass)
default:
err = errors.New("unknown crypt type '" + crypt + "'")
}
return block, err
}
// NewKCPDialer returns a kcp proxy dialer.
func NewKCPDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
return NewKCP(s, d, nil)
}
// NewKCPServer returns a kcp proxy server.
func NewKCPServer(s string, p proxy.Proxy) (proxy.Server, error) {
schemes := strings.SplitN(s, ",", 2)
k, err := NewKCP(schemes[0], nil, p)
if err != nil {
return nil, err
}
if len(schemes) > 1 {
k.server, err = proxy.ServerFromURL(schemes[1], p)
if err != nil {
return nil, err
}
}
return k, nil
}
// ListenAndServe listens on server's addr and serves connections.
func (s *KCP) ListenAndServe() {
l, err := kcp.ListenWithOptions(s.addr, s.block, s.dataShards, s.parityShards)
if err != nil {
log.Fatalf("[kcp] failed to listen on %s: %v", s.addr, err)
return
}
defer l.Close()
log.F("[kcp] listening on %s", s.addr)
for {
c, err := l.AcceptKCP()
if err != nil {
log.F("[kcp] failed to accept: %v", err)
continue
}
s.setParams(c)
go s.Serve(c)
}
}
// Serve serves connections.
func (s *KCP) Serve(c net.Conn) {
if s.server != nil {
s.server.Serve(c)
return
}
defer c.Close()
rc, dialer, err := s.proxy.Dial("tcp", "")
if err != nil {
log.F("[kcp] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), s.addr, dialer.Addr(), err)
s.proxy.Record(dialer, false)
return
}
defer rc.Close()
log.F("[kcp] %s <-> %s", c.RemoteAddr(), dialer.Addr())
if err = proxy.Relay(c, rc); err != nil {
log.F("[kcp] %s <-> %s, relay error: %v", c.RemoteAddr(), dialer.Addr(), err)
// record remote conn failure only
if !strings.Contains(err.Error(), s.addr) {
s.proxy.Record(dialer, false)
}
}
}
// Addr returns forwarder's address.
func (s *KCP) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// Dial connects to the address addr on the network net via the proxy.
func (s *KCP) Dial(network, addr string) (net.Conn, error) {
// NOTE: kcp uses udp, we should dial remote server directly here
c, err := kcp.DialWithOptions(s.addr, s.block, s.dataShards, s.parityShards)
if err != nil {
log.F("[kcp] dial to %s error: %s", s.addr, err)
return nil, err
}
s.setParams(c)
c.SetDSCP(0)
c.SetReadBuffer(4194304)
c.SetWriteBuffer(4194304)
return c, err
}
// DialUDP connects to the given address via the proxy.
func (s *KCP) DialUDP(network, addr string) (net.PacketConn, error) {
return nil, proxy.ErrNotSupported
}
func (s *KCP) setParams(c *kcp.UDPSession) {
// TODO: change them to customizable later?
c.SetStreamMode(true)
c.SetWriteDelay(false)
switch s.mode {
case "normal":
c.SetNoDelay(0, 40, 2, 1)
case "fast":
c.SetNoDelay(0, 30, 2, 1)
case "fast2":
c.SetNoDelay(1, 20, 2, 1)
case "fast3":
c.SetNoDelay(1, 10, 2, 1)
default:
log.F("[kcp] unkonw mode: %s, use fast mode instead", s.mode)
c.SetNoDelay(0, 30, 2, 1)
}
c.SetWindowSize(1024, 1024)
c.SetMtu(1350)
c.SetACKNoDelay(true)
}
func init() {
proxy.AddUsage("kcp", `
KCP scheme:
kcp://CRYPT:KEY@host:port[?dataShards=NUM&parityShards=NUM&mode=MODE]
Available crypt types for KCP:
none, sm4, tea, xor, aes, aes-128, aes-192, blowfish, twofish, cast5, 3des, xtea, salsa20
Available modes for KCP:
fast, fast2, fast3, normal, default: fast
`)
}

View File

@ -1,78 +1,70 @@
package mixed
import (
"bytes"
"net"
"net/url"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
"github.com/nadoo/glider/proxy/http"
"github.com/nadoo/glider/proxy/socks5"
)
// https://www.ietf.org/rfc/rfc2616.txt, http methods must be uppercase.
var httpMethods = [...][]byte{
[]byte("GET"),
[]byte("POST"),
[]byte("PUT"),
[]byte("DELETE"),
[]byte("CONNECT"),
[]byte("HEAD"),
[]byte("OPTIONS"),
[]byte("TRACE"),
}
// MixedProxy struct
type MixedProxy struct {
dialer proxy.Dialer
// Mixed struct.
type Mixed struct {
proxy proxy.Proxy
addr string
http *http.HTTP
socks5 *socks5.SOCKS5
httpServer *http.HTTP
socks5Server *socks5.Socks5
}
func init() {
proxy.RegisterServer("mixed", NewMixedProxyServer)
proxy.RegisterServer("mixed", NewMixedServer)
}
// NewMixedProxy returns a mixed proxy.
func NewMixedProxy(s string, dialer proxy.Dialer) (*MixedProxy, error) {
// NewMixed returns a mixed proxy.
func NewMixed(s string, p proxy.Proxy) (*Mixed, error) {
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
return nil, err
}
p := &MixedProxy{
dialer: dialer,
m := &Mixed{
proxy: p,
addr: u.Host,
}
p.http, _ = http.NewHTTP(s, dialer)
p.socks5, _ = socks5.NewSOCKS5(s, dialer)
return p, nil
}
// NewMixedProxyServer returns a mixed proxy server.
func NewMixedProxyServer(s string, dialer proxy.Dialer) (proxy.Server, error) {
return NewMixedProxy(s, dialer)
}
// ListenAndServe .
func (p *MixedProxy) ListenAndServe() {
go p.socks5.ListenAndServeUDP()
l, err := net.Listen("tcp", p.addr)
m.httpServer, err = http.NewHTTP(s, nil, p)
if err != nil {
log.F("[mixed] failed to listen on %s: %v", p.addr, err)
return nil, err
}
m.socks5Server, err = socks5.NewSocks5(s, nil, p)
if err != nil {
return nil, err
}
return m, nil
}
// NewMixedServer returns a mixed server.
func NewMixedServer(s string, p proxy.Proxy) (proxy.Server, error) {
return NewMixed(s, p)
}
// ListenAndServe listens on server's addr and serves connections.
func (m *Mixed) ListenAndServe() {
go m.socks5Server.ListenAndServeUDP()
l, err := net.Listen("tcp", m.addr)
if err != nil {
log.Fatalf("[mixed] failed to listen on %s: %v", m.addr, err)
return
}
log.F("[mixed] listening TCP on %s", p.addr)
log.F("[mixed] http & socks5 server listening TCP on %s", m.addr)
for {
c, err := l.Accept()
@ -81,47 +73,18 @@ func (p *MixedProxy) ListenAndServe() {
continue
}
go p.Serve(c)
go m.Serve(c)
}
}
// Serve .
func (p *MixedProxy) Serve(c net.Conn) {
defer c.Close()
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
cc := conn.NewConn(c)
if p.socks5 != nil {
head, err := cc.Peek(1)
if err != nil {
log.F("[mixed] peek error: %s", err)
return
}
// check socks5, client send socksversion: 5 as the first byte
// Serve serves connections.
func (m *Mixed) Serve(c net.Conn) {
conn := proxy.NewConn(c)
if head, err := conn.Peek(1); err == nil {
if head[0] == socks5.Version {
p.socks5.ServeTCP(cc)
m.socks5Server.Serve(conn)
return
}
}
if p.http != nil {
head, err := cc.Peek(8)
if err != nil {
log.F("[mixed] peek error: %s", err)
return
}
for _, method := range httpMethods {
if bytes.HasPrefix(head, method) {
p.http.Serve(cc)
return
}
}
}
m.httpServer.Serve(conn)
}

82
proxy/obfs/http.go Normal file
View File

@ -0,0 +1,82 @@
package obfs
import (
"bufio"
"crypto/rand"
"encoding/base64"
"io"
"net"
"github.com/nadoo/glider/pkg/pool"
)
// HTTPObfs struct
type HTTPObfs struct {
obfsHost string
obfsURI string
obfsUA string
}
// NewHTTPObfs returns a HTTPObfs object
func NewHTTPObfs(obfsHost, obfsURI, obfsUA string) *HTTPObfs {
return &HTTPObfs{obfsHost, obfsURI, obfsUA}
}
// HTTPObfsConn struct
type HTTPObfsConn struct {
*HTTPObfs
net.Conn
reader io.Reader
}
// NewConn returns a new obfs connection
func (p *HTTPObfs) NewConn(c net.Conn) (net.Conn, error) {
cc := &HTTPObfsConn{
Conn: c,
HTTPObfs: p,
}
// send http header to remote server
_, err := cc.writeHeader()
return cc, err
}
func (c *HTTPObfsConn) writeHeader() (int, error) {
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
buf.WriteString("GET " + c.obfsURI + " HTTP/1.1\r\n")
buf.WriteString("Host: " + c.obfsHost + "\r\n")
buf.WriteString("User-Agent: " + c.obfsUA + "\r\n")
buf.WriteString("Upgrade: websocket\r\n")
buf.WriteString("Connection: Upgrade\r\n")
b := pool.GetBuffer(16)
rand.Read(b)
buf.WriteString("Sec-WebSocket-Key: " + base64.StdEncoding.EncodeToString(b) + "\r\n")
pool.PutBuffer(b)
buf.WriteString("\r\n")
return c.Conn.Write(buf.Bytes())
}
func (c *HTTPObfsConn) Read(b []byte) (n int, err error) {
if c.reader == nil {
r := bufio.NewReader(c.Conn)
c.reader = r
for {
l, _, err := r.ReadLine()
if err != nil {
return 0, err
}
if len(l) == 0 {
break
}
}
}
return c.reader.Read(b)
}

121
proxy/obfs/obfs.go Normal file
View File

@ -0,0 +1,121 @@
// Package obfs implements simple-obfs of ss
package obfs
import (
"errors"
"net"
"net/url"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
)
// Obfs struct.
type Obfs struct {
dialer proxy.Dialer
addr string
obfsType string
obfsHost string
obfsURI string
obfsUA string
obfsConn func(c net.Conn) (net.Conn, error)
}
func init() {
proxy.RegisterDialer("simple-obfs", NewObfsDialer)
}
// NewObfs returns a proxy struct.
func NewObfs(s string, d proxy.Dialer) (*Obfs, error) {
u, err := url.Parse(s)
if err != nil {
log.F("parse url err: %s", err)
return nil, err
}
addr := u.Host
query := u.Query()
obfsType := query.Get("type")
if obfsType == "" {
obfsType = "http"
}
obfsHost := query.Get("host")
if obfsHost == "" {
return nil, errors.New("[obfs] host cannot be null")
}
obfsURI := query.Get("uri")
if obfsURI == "" {
obfsURI = "/"
}
obfsUA := query.Get("ua")
if obfsUA == "" {
obfsUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36"
}
p := &Obfs{
dialer: d,
addr: addr,
obfsType: obfsType,
obfsHost: obfsHost,
obfsURI: obfsURI,
obfsUA: obfsUA,
}
switch obfsType {
case "http":
httpObfs := NewHTTPObfs(obfsHost, obfsURI, obfsUA)
p.obfsConn = httpObfs.NewConn
case "tls":
tlsObfs := NewTLSObfs(obfsHost)
p.obfsConn = tlsObfs.NewConn
default:
return nil, errors.New("[obfs] unknown obfs type: " + obfsType)
}
return p, nil
}
// NewObfsDialer returns a proxy dialer.
func NewObfsDialer(s string, dialer proxy.Dialer) (proxy.Dialer, error) {
return NewObfs(s, dialer)
}
// Addr returns forwarder's address.
func (s *Obfs) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// Dial connects to the address addr on the network net via the proxy.
func (s *Obfs) Dial(network, addr string) (net.Conn, error) {
c, err := s.dialer.Dial("tcp", s.addr)
if err != nil {
log.F("[obfs] dial to %s error: %s", s.addr, err)
return nil, err
}
return s.obfsConn(c)
}
// DialUDP connects to the given address via the proxy.
func (s *Obfs) DialUDP(network, addr string) (net.PacketConn, error) {
return nil, proxy.ErrNotSupported
}
func init() {
proxy.AddUsage("simple-obfs", `
Simple-Obfs scheme:
simple-obfs://host:port[?type=TYPE&host=HOST&uri=URI&ua=UA]
Available types for simple-obfs:
http, tls
`)
}

265
proxy/obfs/tls.go Normal file
View File

@ -0,0 +1,265 @@
// https://www.rfc-editor.org/rfc/rfc5246
// https://golang.org/src/crypto/tls/handshake_messages.go
// NOTE:
// The official obfs-server only checks 6 static bytes of client hello packet,
// so if we send a malformed packet, e.g: set a wrong length number of extensions,
// obfs-server will treat it as a correct packet, but in wireshak, it's malformed.
package obfs
import (
"bufio"
"bytes"
"crypto/rand"
"encoding/binary"
"io"
"net"
"time"
"github.com/nadoo/glider/pkg/pool"
)
const (
lenSize = 2
chunkSize = 1 << 13 // 8192
)
// TLSObfs struct
type TLSObfs struct {
obfsHost string
}
// NewTLSObfs returns a TLSObfs object
func NewTLSObfs(obfsHost string) *TLSObfs {
return &TLSObfs{obfsHost: obfsHost}
}
// TLSObfsConn struct
type TLSObfsConn struct {
*TLSObfs
net.Conn
reqSent bool
reader *bufio.Reader
buf [lenSize]byte
leftBytes int
}
// NewConn returns a new obfs connection
func (p *TLSObfs) NewConn(c net.Conn) (net.Conn, error) {
cc := &TLSObfsConn{Conn: c, TLSObfs: p}
return cc, nil
}
func (c *TLSObfsConn) Write(b []byte) (int, error) {
if !c.reqSent {
c.reqSent = true
return c.handshake(b)
}
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
n := len(b)
for i := 0; i < n; i += chunkSize {
buf.Reset()
end := min(i+chunkSize, n)
buf.Write([]byte{0x17, 0x03, 0x03})
binary.Write(buf, binary.BigEndian, uint16(len(b[i:end])))
buf.Write(b[i:end])
_, err := c.Conn.Write(buf.Bytes())
if err != nil {
return 0, err
}
}
return n, nil
}
func (c *TLSObfsConn) Read(b []byte) (int, error) {
if c.reader == nil {
c.reader = bufio.NewReader(c.Conn)
// Server Hello
// TLSv1.2 Record Layer: Handshake Protocol: Server Hello (96 bytes)
// TLSv1.2 Record Layer: Change Cipher Spec Protocol: Change Cipher Spec (6 bytes)
c.reader.Discard(102)
}
if c.leftBytes == 0 {
// TLSv1.2 Record Layer:
// 1st packet: handshake encrypted message / following packets: application data
// 1 byte: Content Type: Handshake (22) / Application Data (23)
// 2 bytes: Version: TLS 1.2 (0x0303)
c.reader.Discard(3)
// get length
_, err := io.ReadFull(c.reader, c.buf[:lenSize])
if err != nil {
return 0, err
}
c.leftBytes = int(binary.BigEndian.Uint16(c.buf[:lenSize]))
}
readLen := len(b)
if readLen > c.leftBytes {
readLen = c.leftBytes
}
m, err := c.reader.Read(b[:readLen])
if err != nil {
return 0, err
}
c.leftBytes -= m
return m, nil
}
func (c *TLSObfsConn) handshake(b []byte) (int, error) {
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
bufExt := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(bufExt)
bufHello := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(bufHello)
// prepare extension & clientHello content
extension(b, c.obfsHost, bufExt)
clientHello(bufHello)
// prepare lengths
extLen := bufExt.Len()
helloLen := bufHello.Len() + 2 + extLen // 2: len(extContentLength)
handshakeLen := 4 + helloLen // 1: len(0x01) + 3: len(clientHelloContentLength)
// TLS Record Layer Begin
// Content Type: Handshake (22)
buf.WriteByte(0x16)
// Version: TLS 1.0 (0x0301)
buf.Write([]byte{0x03, 0x01})
// length
binary.Write(buf, binary.BigEndian, uint16(handshakeLen))
// Handshake Begin
// Handshake Type: Client Hello (1)
buf.WriteByte(0x01)
// length: uint24(3 bytes), but golang doesn't have this type
buf.Write([]byte{uint8(helloLen >> 16), uint8(helloLen >> 8), uint8(helloLen)})
// clientHello content
buf.Write(bufHello.Bytes())
// Extension Begin
// ext content length
binary.Write(buf, binary.BigEndian, uint16(extLen))
// ext content
buf.Write(bufExt.Bytes())
_, err := c.Conn.Write(buf.Bytes())
if err != nil {
return 0, err
}
return len(b), nil
}
func clientHello(buf *bytes.Buffer) {
// Version: TLS 1.2 (0x0303)
buf.Write([]byte{0x03, 0x03})
// Random
// https://tools.ietf.org/id/draft-mathewson-no-gmtunixtime-00.txt
// NOTE:
// Most tls implementations do not deal with the first 4 bytes unix time,
// clients do not send current time, and server do not check it,
// golang tls client and chrome browser send random bytes instead.
//
binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix()))
random := make([]byte, 28)
// The above 2 lines of codes was added to make it compatible with some server implementation,
// if we don't need the compatibility, just use the following code instead.
// random := make([]byte, 32)
rand.Read(random)
buf.Write(random)
// Session ID Length: 32
buf.WriteByte(32)
// Session ID
sessionID := make([]byte, 32)
rand.Read(sessionID)
buf.Write(sessionID)
// https://github.com/shadowsocks/simple-obfs/blob/7659eeccf473aa41eb294e92c32f8f60a8747325/src/obfs_tls.c#L57
// Cipher Suites Length: 56
binary.Write(buf, binary.BigEndian, uint16(56))
// Cipher Suites (28 suites)
buf.Write([]byte{
0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f,
0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a,
0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d,
0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff,
})
// Compression Methods Length: 1
buf.WriteByte(0x01)
// Compression Methods (1 method)
buf.WriteByte(0x00)
}
func extension(b []byte, server string, buf *bytes.Buffer) {
// Extension: SessionTicket TLS
buf.Write([]byte{0x00, 0x23}) // type
// NOTE: send some data in sessionticket, the server will treat it as data too
binary.Write(buf, binary.BigEndian, uint16(len(b))) // length
buf.Write(b)
// Extension: server_name
buf.Write([]byte{0x00, 0x00}) // type
binary.Write(buf, binary.BigEndian, uint16(len(server)+5)) // length
binary.Write(buf, binary.BigEndian, uint16(len(server)+3)) // Server Name list length
buf.WriteByte(0x00) // Server Name Type: host_name (0)
binary.Write(buf, binary.BigEndian, uint16(len(server))) // Server Name length
buf.WriteString(server)
// https://github.com/shadowsocks/simple-obfs/blob/7659eeccf473aa41eb294e92c32f8f60a8747325/src/obfs_tls.c#L88
// Extension: ec_point_formats (len=4)
buf.Write([]byte{0x00, 0x0b}) // type
binary.Write(buf, binary.BigEndian, uint16(4)) // length
buf.WriteByte(0x03) // format length
buf.Write([]byte{0x01, 0x00, 0x02})
// Extension: supported_groups (len=10)
buf.Write([]byte{0x00, 0x0a}) // type
binary.Write(buf, binary.BigEndian, uint16(10)) // length
binary.Write(buf, binary.BigEndian, uint16(8)) // Supported Groups List Length: 8
buf.Write([]byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18})
// Extension: signature_algorithms (len=32)
buf.Write([]byte{0x00, 0x0d}) // type
binary.Write(buf, binary.BigEndian, uint16(32)) // length
binary.Write(buf, binary.BigEndian, uint16(30)) // Signature Hash Algorithms Length: 30
buf.Write([]byte{
0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02,
0x04, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03,
})
// Extension: encrypt_then_mac (len=0)
buf.Write([]byte{0x00, 0x16}) // type
binary.Write(buf, binary.BigEndian, uint16(0)) // length
// Extension: extended_master_secret (len=0)
buf.Write([]byte{0x00, 0x17}) // type
binary.Write(buf, binary.BigEndian, uint16(0)) // length
}

46
proxy/proxy.go Normal file
View File

@ -0,0 +1,46 @@
package proxy
import (
"net"
"strings"
)
// Proxy is a dialer manager.
type Proxy interface {
// Dial connects to the given address via the proxy.
Dial(network, addr string) (c net.Conn, dialer Dialer, err error)
// DialUDP connects to the given address via the proxy.
DialUDP(network, addr string) (pc net.PacketConn, dialer UDPDialer, err error)
// Get the dialer by dstAddr.
NextDialer(dstAddr string) Dialer
// Record records result while using the dialer from proxy.
Record(dialer Dialer, success bool)
}
var (
msg strings.Builder
usages = make(map[string]string)
)
// AddUsage adds help message for the named proxy.
func AddUsage(name, usage string) {
usages[name] = usage
msg.WriteString(usage)
msg.WriteString("\n--")
}
// Usage returns help message of the named proxy.
func Usage(name string) string {
if name == "all" {
return msg.String()
}
if usage, ok := usages[name]; ok {
return usage
}
return "can not find usage for: " + name
}

134
proxy/pxyproto/server.go Normal file
View File

@ -0,0 +1,134 @@
package pxyproto
import (
"errors"
"fmt"
"net"
"net/url"
"strings"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
)
func init() {
proxy.RegisterServer("pxyproto", NewPxyProtoServer)
}
// PxyProtoServer struct.
type PxyProtoServer struct {
addr string
proxy proxy.Proxy
server proxy.Server
}
// NewPxyProtoServer returns a PxyProtoServer struct.
func NewPxyProtoServer(s string, p proxy.Proxy) (proxy.Server, error) {
schemes := strings.SplitN(s, ",", 2)
u, err := url.Parse(schemes[0])
if err != nil {
log.F("[pxyproto] parse url err: %s", err)
return nil, err
}
t := &PxyProtoServer{proxy: p, addr: u.Host}
if len(schemes) < 2 {
return nil, errors.New("[pxyproto] you must use pxyproto with a proxy server, e.g: pxyproto://:1234,http://")
}
t.server, err = proxy.ServerFromURL(schemes[1], p)
if err != nil {
return nil, err
}
return t, nil
}
// ListenAndServe listens on server's addr and serves connections.
func (s *PxyProtoServer) ListenAndServe() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.Fatalf("[pxyproto] failed to listen on %s: %v", s.addr, err)
return
}
defer l.Close()
log.F("[pxyproto] listening TCP on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[pxyproto] failed to accept: %v", err)
continue
}
go s.Serve(c)
}
}
// Serve serves a connection.
func (s *PxyProtoServer) Serve(cc net.Conn) {
c, err := newServerConn(cc)
if err != nil {
log.F("[pxyproto] parse header failed, error: %v", err)
cc.Close()
return
}
// log.F("[pxyproto] %s <-> %s <-> %s <-> %s",
// c.RemoteAddr(), c.LocalAddr(), cc.RemoteAddr(), cc.LocalAddr())
if s.server != nil {
s.server.Serve(c)
return
}
}
type serverConn struct {
*proxy.Conn
src, dst net.Addr
}
func newServerConn(c net.Conn) (*serverConn, error) {
sc := &serverConn{
Conn: proxy.NewConn(c),
src: c.RemoteAddr(),
dst: c.LocalAddr(),
}
return sc, sc.parseHeader()
}
// "PROXY TCPx SRC_IP DST_IP SRC_PORT DST_PORT"
func (c *serverConn) parseHeader() error {
line, err := c.Conn.Reader().ReadString('\n')
if err != nil {
return err
}
line = strings.ReplaceAll(line, "\r\n", "")
// log.F("[pxyproto] req header: %s", line)
header := strings.Split(line, " ")
if len(header) != 6 {
return fmt.Errorf("invalid header: %s", line)
}
if header[0] != "PROXY" {
return fmt.Errorf("invalid header: %s", line)
}
c.src, err = net.ResolveTCPAddr("tcp", net.JoinHostPort(header[2], header[4]))
if err != nil {
return fmt.Errorf("parse header: %s, error: %v", line, err)
}
c.dst, err = net.ResolveTCPAddr("tcp", net.JoinHostPort(header[3], header[5]))
if err != nil {
return fmt.Errorf("parse header: %s, error: %v", line, err)
}
return nil
}
func (c *serverConn) LocalAddr() net.Addr { return c.dst }
func (c *serverConn) RemoteAddr() net.Addr { return c.src }

View File

@ -1,40 +1,31 @@
// getOrigDst:
// https://github.com/shadowsocks/go-shadowsocks2/blob/master/tcp_linux.go#L30
package redir
import (
"errors"
"net"
"net/netip"
"net/url"
"strings"
"syscall"
"unsafe"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/common/socks"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
)
const (
// SO_ORIGINAL_DST from linux/include/uapi/linux/netfilter_ipv4.h
SO_ORIGINAL_DST = 80
// IP6T_SO_ORIGINAL_DST from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h
IP6T_SO_ORIGINAL_DST = 80
)
// RedirProxy struct
// RedirProxy struct.
type RedirProxy struct {
dialer proxy.Dialer
proxy proxy.Proxy
addr string
ipv6 bool
}
func init() {
proxy.RegisterServer("redir", NewRedirServer)
proxy.RegisterServer("redir6", NewRedir6Server)
}
// NewRedirProxy returns a redirect proxy.
func NewRedirProxy(s string, dialer proxy.Dialer) (*RedirProxy, error) {
func NewRedirProxy(s string, p proxy.Proxy, ipv6 bool) (*RedirProxy, error) {
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
@ -43,23 +34,29 @@ func NewRedirProxy(s string, dialer proxy.Dialer) (*RedirProxy, error) {
addr := u.Host
r := &RedirProxy{
dialer: dialer,
proxy: p,
addr: addr,
ipv6: ipv6,
}
return r, nil
}
// NewRedirServer returns a redir server.
func NewRedirServer(s string, dialer proxy.Dialer) (proxy.Server, error) {
return NewRedirProxy(s, dialer)
func NewRedirServer(s string, p proxy.Proxy) (proxy.Server, error) {
return NewRedirProxy(s, p, false)
}
// ListenAndServe .
// NewRedir6Server returns a redir server for ipv6.
func NewRedir6Server(s string, p proxy.Proxy) (proxy.Server, error) {
return NewRedirProxy(s, p, true)
}
// ListenAndServe listens on server's addr and serves connections.
func (s *RedirProxy) ListenAndServe() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.F("[redir] failed to listen on %s: %v", s.addr, err)
log.Fatalf("[redir] failed to listen on %s: %v", s.addr, err)
return
}
@ -72,97 +69,93 @@ func (s *RedirProxy) ListenAndServe() {
continue
}
go func() {
defer c.Close()
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
go s.Serve(c)
}
}
tgt, err := getOrigDst(c, false)
// Serve serves connections.
func (s *RedirProxy) Serve(cc net.Conn) {
defer cc.Close()
c, ok := cc.(*net.TCPConn)
if !ok {
log.F("[redir] not a tcp connection, can not chain redir proxy")
return
}
c.SetKeepAlive(true)
tgtAddr, err := getOrigDst(c, s.ipv6)
if err != nil {
log.F("[redir] failed to get target address: %v", err)
return
}
tgt := tgtAddr.String()
rc, err := s.dialer.Dial("tcp", tgt.String())
// loop request
if c.LocalAddr().String() == tgt {
log.F("[redir] %s <-> %s, unallowed request to redir port", c.RemoteAddr(), tgt)
return
}
rc, dialer, err := s.proxy.Dial("tcp", tgt)
if err != nil {
log.F("[redir] failed to connect to target: %v", err)
log.F("[redir] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
return
}
defer rc.Close()
log.F("[redir] %s <-> %s", c.RemoteAddr(), tgt)
log.F("[redir] %s <-> %s via %s", c.RemoteAddr(), tgt, dialer.Addr())
_, _, err = conn.Relay(c, rc)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
return // ignore i/o timeout
if err = proxy.Relay(c, rc); err != nil {
log.F("[redir] %s <-> %s via %s, relay error: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
// record remote conn failure only
if !strings.Contains(err.Error(), s.addr) {
s.proxy.Record(dialer, false)
}
log.F("[redir] relay error: %v", err)
}
}()
}
}
// Get the original destination of a TCP connection.
func getOrigDst(conn net.Conn, ipv6 bool) (socks.Addr, error) {
c, ok := conn.(*net.TCPConn)
if !ok {
return nil, errors.New("only work with TCP connection")
}
f, err := c.File()
func getOrigDst(c *net.TCPConn, ipv6 bool) (netip.AddrPort, error) {
rc, err := c.SyscallConn()
if err != nil {
return nil, err
return netip.AddrPort{}, err
}
defer f.Close()
fd := f.Fd()
// The File() call above puts both the original socket fd and the file fd in blocking mode.
// Set the file fd back to non-blocking mode and the original socket fd will become non-blocking as well.
// Otherwise blocking I/O will waste OS threads.
if err := syscall.SetNonblock(int(fd), true); err != nil {
return nil, err
}
var addr netip.AddrPort
rc.Control(func(fd uintptr) {
if ipv6 {
return getorigdstIPv6(fd)
addr, err = getorigdstIPv6(fd)
} else {
addr, err = getorigdst(fd)
}
return getorigdst(fd)
})
return addr, err
}
// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c
func getorigdst(fd uintptr) (socks.Addr, error) {
raw := syscall.RawSockaddrInet4{}
func getorigdst(fd uintptr) (netip.AddrPort, error) {
const _SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h
var raw syscall.RawSockaddrInet4
siz := unsafe.Sizeof(raw)
if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil {
return nil, err
if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, _SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil {
return netip.AddrPort{}, err
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks.ATypIP4
copy(addr[1:1+net.IPv4len], raw.Addr[:])
port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian
addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1]
return addr, nil
// NOTE: raw.Port is big-endian, just change it to little-endian
// TODO: improve here when we add big-endian $GOARCH support
port := raw.Port<<8 | raw.Port>>8
return netip.AddrPortFrom(netip.AddrFrom4(raw.Addr), port), nil
}
// Call ipv6_getorigdst() from linux/net/ipv6/netfilter/nf_conntrack_l3proto_ipv6.c
// NOTE: I haven't tried yet but it should work since Linux 3.8.
func getorigdstIPv6(fd uintptr) (socks.Addr, error) {
raw := syscall.RawSockaddrInet6{}
func getorigdstIPv6(fd uintptr) (netip.AddrPort, error) {
const _IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h
var raw syscall.RawSockaddrInet6
siz := unsafe.Sizeof(raw)
if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil {
return nil, err
if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IPV6, _IP6T_SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil {
return netip.AddrPort{}, err
}
addr := make([]byte, 1+net.IPv6len+2)
addr[0] = socks.ATypIP6
copy(addr[1:1+net.IPv6len], raw.Addr[:])
port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian
addr[1+net.IPv6len], addr[1+net.IPv6len+1] = port[0], port[1]
return addr, nil
// NOTE: raw.Port is big-endian, just change it to little-endian
// TODO: improve here when we add big-endian $GOARCH support
port := raw.Port<<8 | raw.Port>>8
return netip.AddrPortFrom(netip.AddrFrom16(raw.Addr), port), nil
}

View File

@ -6,7 +6,7 @@ import (
)
// https://github.com/golang/go/blob/9e6b79a5dfb2f6fe4301ced956419a0da83bd025/src/syscall/syscall_linux_386.go#L196
const GETSOCKOPT = 15
const GETSOCKOPT = 15 // https://golang.org/src/syscall/syscall_linux_386.go#L183
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
var a [6]uintptr

View File

@ -1,4 +1,4 @@
// +build linux,!386
//go:build linux && !386
package redir

46
proxy/reject/reject.go Normal file
View File

@ -0,0 +1,46 @@
// Package reject implements a virtual proxy which always reject requests.
package reject
import (
"errors"
"net"
"github.com/nadoo/glider/proxy"
)
// A Reject represents the base struct of a reject proxy.
type Reject struct{}
func init() {
proxy.RegisterDialer("reject", NewRejectDialer)
}
// NewReject returns a reject proxy, reject://.
func NewReject(s string, d proxy.Dialer) (*Reject, error) {
return &Reject{}, nil
}
// NewRejectDialer returns a reject proxy dialer.
func NewRejectDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
return NewReject(s, d)
}
// Addr returns forwarder's address.
func (s *Reject) Addr() string { return "REJECT" }
// Dial connects to the address addr on the network net via the proxy.
func (s *Reject) Dial(network, addr string) (net.Conn, error) {
return nil, errors.New("REJECT")
}
// DialUDP connects to the given address via the proxy.
func (s *Reject) DialUDP(network, addr string) (net.PacketConn, error) {
return nil, errors.New("REJECT")
}
func init() {
proxy.AddUsage("reject", `
Reject scheme:
reject://
`)
}

View File

@ -2,50 +2,63 @@ package proxy
import (
"errors"
"net/url"
"net"
"sort"
"strings"
"github.com/nadoo/glider/common/log"
)
// Server interface
// Server interface.
type Server interface {
// ListenAndServe as proxy server, use only in server mode.
// ListenAndServe sets up a listener and serve on it
ListenAndServe()
// Serve serves a connection
Serve(c net.Conn)
}
// PacketServer interface.
type PacketServer interface {
ServePacket(pc net.PacketConn)
}
// ServerCreator is a function to create proxy servers.
type ServerCreator func(s string, dialer Dialer) (Server, error)
type ServerCreator func(s string, proxy Proxy) (Server, error)
var (
serverMap = make(map[string]ServerCreator)
serverCreators = make(map[string]ServerCreator)
)
// RegisterServer is used to register a proxy server
// RegisterServer is used to register a proxy server.
func RegisterServer(name string, c ServerCreator) {
serverMap[name] = c
serverCreators[strings.ToLower(name)] = c
}
// ServerFromURL calls the registered creator to create proxy servers.
func ServerFromURL(s string, dialer Dialer) (Server, error) {
// proxy can not be nil.
func ServerFromURL(s string, proxy Proxy) (Server, error) {
if proxy == nil {
return nil, errors.New("ServerFromURL: dialer cannot be nil")
}
if !strings.Contains(s, "://") {
s = "mixed://" + s
}
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
return nil, err
}
if dialer == nil {
dialer = Direct
}
c, ok := serverMap[strings.ToLower(u.Scheme)]
scheme := s[:strings.Index(s, ":")]
c, ok := serverCreators[strings.ToLower(scheme)]
if ok {
return c(s, dialer)
return c(s, proxy)
}
return nil, errors.New("unknown scheme '" + u.Scheme + "'")
return nil, errors.New("unknown scheme '" + scheme + "'")
}
// ServerSchemes returns the registered server schemes.
func ServerSchemes() string {
s := make([]string, 0, len(serverCreators))
for name := range serverCreators {
s = append(s, name)
}
sort.Strings(s)
return strings.Join(s, " ")
}

79
proxy/smux/client.go Normal file
View File

@ -0,0 +1,79 @@
package smux
import (
"net"
"net/url"
"sync"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/smux"
"github.com/nadoo/glider/proxy"
)
// SmuxClient struct.
type SmuxClient struct {
dialer proxy.Dialer
addr string
mu sync.Mutex
session *smux.Session
}
func init() {
proxy.RegisterDialer("smux", NewSmuxDialer)
}
// NewSmuxDialer returns a smux dialer.
func NewSmuxDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
u, err := url.Parse(s)
if err != nil {
log.F("[smux] parse url err: %s", err)
return nil, err
}
c := &SmuxClient{
dialer: d,
addr: u.Host,
}
return c, nil
}
// Addr returns forwarder's address.
func (s *SmuxClient) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// Dial connects to the address addr on the network net via the proxy.
func (s *SmuxClient) Dial(network, addr string) (net.Conn, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.session != nil {
if c, err := s.session.OpenStream(); err == nil {
return c, err
}
s.session.Close()
}
if err := s.initConn(); err != nil {
return nil, err
}
return s.session.OpenStream()
}
// DialUDP connects to the given address via the proxy.
func (s *SmuxClient) DialUDP(network, addr string) (net.PacketConn, error) {
return nil, proxy.ErrNotSupported
}
func (s *SmuxClient) initConn() error {
conn, err := s.dialer.Dial("tcp", s.addr)
if err != nil {
log.F("[smux] dial to %s error: %s", s.addr, err)
return err
}
s.session, err = smux.Client(conn, nil)
return err
}

119
proxy/smux/server.go Normal file
View File

@ -0,0 +1,119 @@
package smux
import (
"net"
"net/url"
"strings"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/smux"
"github.com/nadoo/glider/proxy"
)
// SmuxServer struct.
type SmuxServer struct {
proxy proxy.Proxy
addr string
server proxy.Server
}
func init() {
proxy.RegisterServer("smux", NewSmuxServer)
}
// NewSmuxServer returns a smux transport layer before the real server.
func NewSmuxServer(s string, p proxy.Proxy) (proxy.Server, error) {
schemes := strings.SplitN(s, ",", 2)
u, err := url.Parse(schemes[0])
if err != nil {
log.F("[smux] parse url err: %s", err)
return nil, err
}
m := &SmuxServer{
proxy: p,
addr: u.Host,
}
if len(schemes) > 1 {
m.server, err = proxy.ServerFromURL(schemes[1], p)
if err != nil {
return nil, err
}
}
return m, nil
}
// ListenAndServe listens on server's addr and serves connections.
func (s *SmuxServer) ListenAndServe() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.Fatalf("[smux] failed to listen on %s: %v", s.addr, err)
return
}
defer l.Close()
log.F("[smux] listening mux on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[smux] failed to accept: %v", err)
continue
}
go s.Serve(c)
}
}
// Serve serves a connection.
func (s *SmuxServer) Serve(c net.Conn) {
// we know the internal server will close the connection after serve
// defer c.Close()
session, err := smux.Server(c, nil)
if err != nil {
log.F("[smux] failed to create session: %v", err)
return
}
for {
// Accept a stream
stream, err := session.AcceptStream()
if err != nil {
session.Close()
break
}
go s.ServeStream(stream)
}
}
// ServeStream serves a smux stream.
func (s *SmuxServer) ServeStream(c *smux.Stream) {
if s.server != nil {
s.server.Serve(c)
return
}
defer c.Close()
rc, dialer, err := s.proxy.Dial("tcp", "")
if err != nil {
log.F("[smux] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), s.addr, dialer.Addr(), err)
s.proxy.Record(dialer, false)
return
}
defer rc.Close()
log.F("[smux] %s <-> %s", c.RemoteAddr(), dialer.Addr())
if err = proxy.Relay(c, rc); err != nil {
log.F("[smux] %s <-> %s, relay error: %v", c.RemoteAddr(), dialer.Addr(), err)
// record remote conn failure only
if !strings.Contains(err.Error(), s.addr) {
s.proxy.Record(dialer, false)
}
}
}

10
proxy/smux/smux.go Normal file
View File

@ -0,0 +1,10 @@
package smux
import "github.com/nadoo/glider/proxy"
func init() {
proxy.AddUsage("smux", `
Smux scheme:
smux://host:port
`)
}

195
proxy/socks4/socks4.go Normal file
View File

@ -0,0 +1,195 @@
// https://www.openssh.com/txt/socks4.protocol
// socks4 client
package socks4
import (
"errors"
"io"
"net"
"net/url"
"strconv"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/proxy"
)
const (
// Version is socks4 version number.
Version = 4
// ConnectCommand connect command byte
ConnectCommand = 1
)
// SOCKS4 is a base socks4 struct.
type SOCKS4 struct {
dialer proxy.Dialer
addr string
socks4a bool
}
func init() {
proxy.RegisterDialer("socks4", NewSocks4Dialer)
proxy.RegisterDialer("socks4a", NewSocks4Dialer)
}
// NewSOCKS4 returns a socks4 proxy.
func NewSOCKS4(s string, dialer proxy.Dialer) (*SOCKS4, error) {
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
return nil, err
}
h := &SOCKS4{
dialer: dialer,
addr: u.Host,
socks4a: u.Scheme == "socks4a",
}
return h, nil
}
// NewSocks4Dialer returns a socks4 proxy dialer.
func NewSocks4Dialer(s string, dialer proxy.Dialer) (proxy.Dialer, error) {
return NewSOCKS4(s, dialer)
}
// Addr returns forwarder's address.
func (s *SOCKS4) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// Dial connects to the address addr on the network net via the SOCKS4 proxy.
func (s *SOCKS4) Dial(network, addr string) (net.Conn, error) {
switch network {
case "tcp", "tcp4":
default:
return nil, errors.New("[socks4] no support for connection type " + network)
}
c, err := s.dialer.Dial(network, s.addr)
if err != nil {
log.F("[socks4] dial to %s error: %s", s.addr, err)
return nil, err
}
if err := s.connect(c, addr); err != nil {
c.Close()
return nil, err
}
return c, nil
}
// DialUDP connects to the given address via the proxy.
func (s *SOCKS4) DialUDP(network, addr string) (pc net.PacketConn, err error) {
return nil, proxy.ErrNotSupported
}
func (s *SOCKS4) lookupIP(host string) (ip net.IP, err error) {
ips, err := net.LookupIP(host)
if err != nil {
return
}
if len(ips) == 0 {
err = errors.New("[socks4] Cannot resolve host: " + host)
return
}
ip = ips[0].To4()
if len(ip) != net.IPv4len {
err = errors.New("[socks4] IPv6 is not supported by socks4")
return
}
return
}
// connect takes an existing connection to a socks4 proxy server,
// and commands the server to extend that connection to target,
// which must be a canonical address with a host and port.
func (s *SOCKS4) connect(conn net.Conn, target string) error {
host, portStr, err := net.SplitHostPort(target)
if err != nil {
return err
}
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return errors.New("[socks4] failed to parse port number: " + portStr)
}
const baseBufSize = 8 + 1 // 1 is the len(userid)
bufSize := baseBufSize
var ip net.IP
if ip = net.ParseIP(host); ip == nil {
if s.socks4a {
// The client should set the first three bytes of DSTIP to NULL
// and the last byte to a non-zero value.
ip = []byte{0, 0, 0, 1}
bufSize += len(host) + 1
} else {
ip, err = s.lookupIP(host)
if err != nil {
return err
}
}
} else {
ip = ip.To4()
if ip == nil {
return errors.New("[socks4] IPv6 is not supported by socks4")
}
}
// taken from https://github.com/h12w/socks/blob/master/socks.go and https://en.wikipedia.org/wiki/SOCKS
buf := pool.GetBuffer(bufSize)
defer pool.PutBuffer(buf)
copy(buf, []byte{
Version,
ConnectCommand,
byte(port >> 8), // higher byte of destination port
byte(port), // lower byte of destination port (big endian)
ip[0], ip[1], ip[2], ip[3],
0, // user id
})
if s.socks4a {
copy(buf[baseBufSize:], host)
buf[len(buf)-1] = 0
}
resp := pool.GetBuffer(8)
defer pool.PutBuffer(resp)
if _, err := conn.Write(buf); err != nil {
return errors.New("[socks4] failed to write greeting to socks4 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, resp); err != nil {
return errors.New("[socks4] failed to read greeting from socks4 proxy at " + s.addr + ": " + err.Error())
}
switch resp[1] {
case 0x5a:
// request granted
case 0x5b:
err = errors.New("[socks4] connection request rejected or failed")
case 0x5c:
err = errors.New("[socks4] connection request request failed because client is not running identd (or not reachable from the server)")
case 0x5d:
err = errors.New("[socks4] connection request request failed because client's identd could not confirm the user ID in the request")
default:
err = errors.New("[socks4] connection request failed, unknown error")
}
return err
}
func init() {
proxy.AddUsage("socks4", `
Socks4 scheme:
socks4://host:port
`)
}

179
proxy/socks5/client.go Normal file
View File

@ -0,0 +1,179 @@
package socks5
import (
"errors"
"io"
"net"
"net/netip"
"strconv"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/pkg/socks"
"github.com/nadoo/glider/proxy"
)
func init() {
proxy.RegisterDialer("socks5", NewSocks5Dialer)
}
// NewSocks5Dialer returns a socks5 proxy dialer.
func NewSocks5Dialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
return NewSocks5(s, d, nil)
}
// Addr returns forwarder's address.
func (s *Socks5) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// Dial connects to the address addr on the network net via the SOCKS5 proxy.
func (s *Socks5) Dial(network, addr string) (net.Conn, error) {
c, err := s.dial(network, s.addr)
if err != nil {
log.F("[socks5]: dial to %s error: %s", s.addr, err)
return nil, err
}
if _, err := s.connect(c, addr, socks.CmdConnect); err != nil {
c.Close()
return nil, err
}
return c, nil
}
func (s *Socks5) dial(network, addr string) (net.Conn, error) {
switch network {
case "tcp", "tcp6", "tcp4":
default:
return nil, errors.New("[socks5]: no support for connection type " + network)
}
c, err := s.dialer.Dial(network, s.addr)
if err != nil {
log.F("[socks5]: dial to %s error: %s", s.addr, err)
return nil, err
}
return c, nil
}
// DialUDP connects to the given address via the proxy.
func (s *Socks5) DialUDP(network, addr string) (pc net.PacketConn, err error) {
c, err := s.dial("tcp", s.addr)
if err != nil {
log.F("[socks5] dialudp dial tcp to %s error: %s", s.addr, err)
return nil, err
}
var uAddr socks.Addr
if uAddr, err = s.connect(c, addr, socks.CmdUDPAssociate); err != nil {
c.Close()
return nil, err
}
buf := pool.GetBuffer(socks.MaxAddrLen)
defer pool.PutBuffer(buf)
uAddress := uAddr.String()
h, p, _ := net.SplitHostPort(uAddress)
// if returned bind ip is unspecified
if ip, err := netip.ParseAddr(h); err == nil && ip.IsUnspecified() {
// indicate using conventional addr
h, _, _ = net.SplitHostPort(s.addr)
uAddress = net.JoinHostPort(h, p)
}
pc, err = s.dialer.DialUDP(network, uAddress)
if err != nil {
log.F("[socks5] dialudp to %s error: %s", uAddress, err)
return nil, err
}
writeTo, err := net.ResolveUDPAddr("udp", uAddress)
if err != nil {
log.F("[socks5] resolve addr error: %s", err)
return nil, err
}
return NewPktConn(pc, writeTo, socks.ParseAddr(addr), c), err
}
// connect takes an existing connection to a socks5 proxy server,
// and commands the server to extend that connection to target,
// which must be a canonical address with a host and port.
func (s *Socks5) connect(conn net.Conn, target string, cmd byte) (addr socks.Addr, err error) {
// the size here is just an estimate
buf := pool.GetBuffer(socks.MaxAddrLen)
defer pool.PutBuffer(buf)
buf = append(buf[:0], Version)
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
buf = append(buf, 2 /* num auth methods */, socks.AuthNone, socks.AuthPassword)
} else {
buf = append(buf, 1 /* num auth methods */, socks.AuthNone)
}
if _, err := conn.Write(buf); err != nil {
return addr, errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return addr, errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[0] != Version {
return addr, errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
}
if buf[1] == 0xff {
return addr, errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
}
if buf[1] == socks.AuthPassword {
buf = buf[:0]
buf = append(buf, 1 /* password protocol version */)
buf = append(buf, uint8(len(s.user)))
buf = append(buf, s.user...)
buf = append(buf, uint8(len(s.password)))
buf = append(buf, s.password...)
if _, err := conn.Write(buf); err != nil {
return addr, errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return addr, errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[1] != 0 {
return addr, errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
}
}
buf = buf[:0]
buf = append(buf, Version, cmd, 0 /* reserved */)
buf = append(buf, socks.ParseAddr(target)...)
if _, err := conn.Write(buf); err != nil {
return addr, errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
// read VER REP RSV
if _, err := io.ReadFull(conn, buf[:3]); err != nil {
return addr, errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
failure := "unknown error"
if int(buf[1]) < len(socks.Errors) {
failure = socks.Errors[buf[1]].Error()
}
if len(failure) > 0 {
return addr, errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
}
return socks.ReadAddr(conn)
}

View File

@ -1,42 +1,40 @@
package socks5
import (
"errors"
"net"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/common/socks"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/pkg/socks"
)
// PktConn .
type PktConn struct {
net.PacketConn
writeAddr net.Addr // write to and read from addr
tgtAddr socks.Addr
tgtHeader bool
ctrlConn net.Conn // tcp control conn
writeTo net.Addr // write to and read from addr
target socks.Addr
}
// NewPktConn returns a PktConn
func NewPktConn(c net.PacketConn, writeAddr net.Addr, tgtAddr socks.Addr, tgtHeader bool, ctrlConn net.Conn) *PktConn {
// NewPktConn returns a PktConn, the writeAddr must be *net.UDPAddr or *net.UnixAddr.
func NewPktConn(c net.PacketConn, writeAddr net.Addr, targetAddr socks.Addr, ctrlConn net.Conn) *PktConn {
pc := &PktConn{
PacketConn: c,
writeAddr: writeAddr,
tgtAddr: tgtAddr,
tgtHeader: tgtHeader,
ctrlConn: ctrlConn}
writeTo: writeAddr,
target: targetAddr,
ctrlConn: ctrlConn,
}
if ctrlConn != nil {
go func() {
buf := []byte{}
buf := pool.GetBuffer(1)
defer pool.PutBuffer(buf)
for {
_, err := ctrlConn.Read(buf)
if err, ok := err.(net.Error); ok && err.Timeout() {
continue
}
log.F("[socks5] dialudp udp associate end")
// log.F("[socks5] dialudp udp associate end")
return
}
}()
@ -45,48 +43,78 @@ func NewPktConn(c net.PacketConn, writeAddr net.Addr, tgtAddr socks.Addr, tgtHea
return pc
}
// ReadFrom overrides the original function from net.PacketConn
// ReadFrom overrides the original function from net.PacketConn.
func (pc *PktConn) ReadFrom(b []byte) (int, net.Addr, error) {
if !pc.tgtHeader {
return pc.PacketConn.ReadFrom(b)
n, _, target, err := pc.readFrom(b)
return n, target, err
}
buf := make([]byte, len(b))
func (pc *PktConn) readFrom(b []byte) (int, net.Addr, net.Addr, error) {
buf := pool.GetBuffer(len(b))
defer pool.PutBuffer(buf)
n, raddr, err := pc.PacketConn.ReadFrom(buf)
if err != nil {
return n, raddr, err
return n, raddr, nil, err
}
// https://tools.ietf.org/html/rfc1928#section-7
if n < 3 {
return n, raddr, nil, errors.New("not enough size to get addr")
}
// https://www.rfc-editor.org/rfc/rfc1928#section-7
// +----+------+------+----------+----------+----------+
// |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
// +----+------+------+----------+----------+----------+
// | 2 | 1 | 1 | Variable | 2 | Variable |
// +----+------+------+----------+----------+----------+
tgtAddr := socks.SplitAddr(buf[3:])
copy(b, buf[3+len(tgtAddr):])
//test
if pc.writeAddr == nil {
pc.writeAddr = raddr
tgtAddr := socks.SplitAddr(buf[3:n])
if tgtAddr == nil {
return n, raddr, nil, errors.New("can not get target addr")
}
if pc.tgtAddr == nil {
pc.tgtAddr = tgtAddr
target, err := net.ResolveUDPAddr("udp", tgtAddr.String())
if err != nil {
return n, raddr, nil, errors.New("wrong target addr")
}
return n - len(tgtAddr) - 3, raddr, err
if pc.writeTo == nil {
pc.writeTo = raddr
}
// WriteTo overrides the original function from net.PacketConn
if pc.target == nil {
pc.target = make([]byte, len(tgtAddr))
copy(pc.target, tgtAddr)
}
n = copy(b, buf[3+len(tgtAddr):n])
return n, raddr, target, err
}
// WriteTo overrides the original function from net.PacketConn.
func (pc *PktConn) WriteTo(b []byte, addr net.Addr) (int, error) {
if !pc.tgtHeader {
return pc.PacketConn.WriteTo(b, addr)
target := pc.target
if addr != nil {
target = socks.ParseAddr(addr.String())
}
buf := append([]byte{0, 0, 0}, pc.tgtAddr...)
buf = append(buf, b[:]...)
return pc.PacketConn.WriteTo(buf, pc.writeAddr)
if target == nil {
return 0, errors.New("invalid addr")
}
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
buf.Write([]byte{0, 0, 0})
tgtLen, _ := buf.Write(target)
buf.Write(b)
n, err := pc.PacketConn.WriteTo(buf.Bytes(), pc.writeTo)
if n > tgtLen+3 {
return n - tgtLen - 3, err
}
return 0, err
}
// Close .

299
proxy/socks5/server.go Normal file
View File

@ -0,0 +1,299 @@
package socks5
import (
"errors"
"io"
"net"
"strings"
"sync"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/pkg/socks"
"github.com/nadoo/glider/proxy"
)
var nm sync.Map
func init() {
proxy.RegisterServer("socks5", NewSocks5Server)
}
// NewSocks5Server returns a socks5 proxy server.
func NewSocks5Server(s string, p proxy.Proxy) (proxy.Server, error) {
return NewSocks5(s, nil, p)
}
// ListenAndServe serves socks5 requests.
func (s *Socks5) ListenAndServe() {
go s.ListenAndServeUDP()
s.ListenAndServeTCP()
}
// ListenAndServeTCP listen and serve on tcp port.
func (s *Socks5) ListenAndServeTCP() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.Fatalf("[socks5] failed to listen on %s: %v", s.addr, err)
return
}
log.F("[socks5] listening TCP on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[socks5] failed to accept: %v", err)
continue
}
go s.Serve(c)
}
}
// Serve serves a connection.
func (s *Socks5) Serve(c net.Conn) {
defer c.Close()
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
tgt, err := s.handshake(c)
if err != nil {
// UDP: keep the connection until disconnect then free the UDP socket
if err == socks.Errors[9] {
buf := pool.GetBuffer(1)
defer pool.PutBuffer(buf)
// block here
for {
_, err := c.Read(buf)
if err, ok := err.(net.Error); ok && err.Timeout() {
continue
}
// log.F("[socks5] servetcp udp associate end")
return
}
}
log.F("[socks5] failed in handshake with %s: %v", c.RemoteAddr(), err)
return
}
rc, dialer, err := s.proxy.Dial("tcp", tgt.String())
if err != nil {
log.F("[socks5] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
return
}
defer rc.Close()
log.F("[socks5] %s <-> %s via %s", c.RemoteAddr(), tgt, dialer.Addr())
if err = proxy.Relay(c, rc); err != nil {
log.F("[socks5] %s <-> %s via %s, relay error: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
// record remote conn failure only
if !strings.Contains(err.Error(), s.addr) {
s.proxy.Record(dialer, false)
}
}
}
// ListenAndServeUDP serves udp requests.
func (s *Socks5) ListenAndServeUDP() {
lc, err := net.ListenPacket("udp", s.addr)
if err != nil {
log.Fatalf("[socks5] failed to listen on UDP %s: %v", s.addr, err)
return
}
defer lc.Close()
log.F("[socks5] listening UDP on %s", s.addr)
s.ServePacket(lc)
}
// ServePacket implements proxy.PacketServer.
func (s *Socks5) ServePacket(pc net.PacketConn) {
for {
c := NewPktConn(pc, nil, nil, nil)
buf := pool.GetBuffer(proxy.UDPBufSize)
n, srcAddr, dstAddr, err := c.readFrom(buf)
if err != nil {
log.F("[socks5u] remote read error: %v", err)
continue
}
var session *Session
sessionKey := srcAddr.String()
v, ok := nm.Load(sessionKey)
if !ok || v == nil {
session = newSession(sessionKey, srcAddr, dstAddr, c)
nm.Store(sessionKey, session)
go s.serveSession(session)
} else {
session = v.(*Session)
}
session.msgCh <- message{dstAddr, buf[:n]}
}
}
func (s *Socks5) serveSession(session *Session) {
dstPC, dialer, err := s.proxy.DialUDP("udp", session.srcPC.target.String())
if err != nil {
log.F("[socks5u] remote dial error: %v", err)
nm.Delete(session.key)
return
}
defer dstPC.Close()
go func() {
proxy.CopyUDP(session.srcPC, nil, dstPC, 2*time.Minute, 5*time.Second)
nm.Delete(session.key)
close(session.finCh)
}()
log.F("[socks5u] %s <-> %s via %s", session.src, session.srcPC.target, dialer.Addr())
for {
select {
case msg := <-session.msgCh:
_, err = dstPC.WriteTo(msg.msg, msg.dst)
if err != nil {
log.F("[socks5u] writeTo %s error: %v", msg.dst, err)
}
pool.PutBuffer(msg.msg)
msg.msg = nil
case <-session.finCh:
return
}
}
}
type message struct {
dst net.Addr
msg []byte
}
// Session is a udp session
type Session struct {
key string
src net.Addr
dst net.Addr
srcPC *PktConn
msgCh chan message
finCh chan struct{}
}
func newSession(key string, src, dst net.Addr, srcPC *PktConn) *Session {
return &Session{key, src, dst, srcPC, make(chan message, 32), make(chan struct{})}
}
// Handshake fast-tracks SOCKS initialization to get target address to connect.
func (s *Socks5) handshake(c net.Conn) (socks.Addr, error) {
// Read RFC 1928 for request and reply structure and sizes
buf := pool.GetBuffer(socks.MaxAddrLen)
defer pool.PutBuffer(buf)
// read VER, NMETHODS, METHODS
if _, err := io.ReadFull(c, buf[:2]); err != nil {
return nil, err
}
nmethods := buf[1]
if _, err := io.ReadFull(c, buf[:nmethods]); err != nil {
return nil, err
}
// write VER METHOD
if s.user != "" && s.password != "" {
_, err := c.Write([]byte{Version, socks.AuthPassword})
if err != nil {
return nil, err
}
_, err = io.ReadFull(c, buf[:2])
if err != nil {
return nil, err
}
// Get username
userLen := int(buf[1])
if userLen <= 0 {
c.Write([]byte{1, 1})
return nil, errors.New("auth failed: wrong username length")
}
if _, err := io.ReadFull(c, buf[:userLen]); err != nil {
return nil, errors.New("auth failed: cannot get username")
}
user := string(buf[:userLen])
// Get password
_, err = c.Read(buf[:1])
if err != nil {
return nil, errors.New("auth failed: cannot get password len")
}
passLen := int(buf[0])
if passLen <= 0 {
c.Write([]byte{1, 1})
return nil, errors.New("auth failed: wrong password length")
}
_, err = io.ReadFull(c, buf[:passLen])
if err != nil {
return nil, errors.New("auth failed: cannot get password")
}
pass := string(buf[:passLen])
// Verify
if user != s.user || pass != s.password {
_, err = c.Write([]byte{1, 1})
if err != nil {
return nil, err
}
return nil, errors.New("auth failed, authinfo: " + user + ":" + pass)
}
// Response auth state
_, err = c.Write([]byte{1, 0})
if err != nil {
return nil, err
}
} else if _, err := c.Write([]byte{Version, socks.AuthNone}); err != nil {
return nil, err
}
// read VER CMD RSV ATYP DST.ADDR DST.PORT
if _, err := io.ReadFull(c, buf[:3]); err != nil {
return nil, err
}
cmd := buf[1]
addr, err := socks.ReadAddr(c)
if err != nil {
return nil, err
}
switch cmd {
case socks.CmdConnect:
_, err = c.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}) // SOCKS v5, reply succeeded
case socks.CmdUDPAssociate:
listenAddr := socks.ParseAddr(c.LocalAddr().String())
if listenAddr == nil { // maybe it's unix socket
listenAddr = socks.ParseAddr("127.0.0.1:0")
}
_, err = c.Write(append([]byte{5, 0, 0}, listenAddr...)) // SOCKS v5, reply succeeded
if err != nil {
return nil, socks.Errors[7]
}
err = socks.Errors[9]
default:
return nil, socks.Errors[7]
}
return addr, err // skip VER, CMD, RSV fields
}

View File

@ -1,4 +1,4 @@
// https://tools.ietf.org/html/rfc1928
// https://www.rfc-editor.org/rfc/rfc1928
// socks5 client:
// https://github.com/golang/net/tree/master/proxy
@ -6,45 +6,31 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// socks5 server:
// https://github.com/shadowsocks/go-shadowsocks2/tree/master/socks
// Package socks5 implements a socks5 proxy.
package socks5
import (
"errors"
"io"
"net"
"net/url"
"strconv"
"sync"
"time"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/common/socks"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
)
// Version is socks5 version number
// Version is socks5 version number.
const Version = 5
// SOCKS5 struct
type SOCKS5 struct {
// Socks5 is a base socks5 struct.
type Socks5 struct {
dialer proxy.Dialer
proxy proxy.Proxy
addr string
user string
password string
}
func init() {
proxy.RegisterDialer("socks5", NewSocks5Dialer)
proxy.RegisterServer("socks5", NewSocks5Server)
}
// NewSOCKS5 returns a Proxy that makes SOCKS v5 connections to the given address
// with an optional username and password. See RFC 1928.
func NewSOCKS5(s string, dialer proxy.Dialer) (*SOCKS5, error) {
// NewSocks5 returns a Proxy that makes SOCKS v5 connections to the given address.
// with an optional username and password. (RFC 1928)
func NewSocks5(s string, d proxy.Dialer, p proxy.Proxy) (*Socks5, error) {
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
@ -55,8 +41,9 @@ func NewSOCKS5(s string, dialer proxy.Dialer) (*SOCKS5, error) {
user := u.User.Username()
pass, _ := u.User.Password()
h := &SOCKS5{
dialer: dialer,
h := &Socks5{
dialer: d,
proxy: p,
addr: addr,
user: user,
password: pass,
@ -65,409 +52,9 @@ func NewSOCKS5(s string, dialer proxy.Dialer) (*SOCKS5, error) {
return h, nil
}
// NewSocks5Dialer returns a socks5 proxy dialer.
func NewSocks5Dialer(s string, dialer proxy.Dialer) (proxy.Dialer, error) {
return NewSOCKS5(s, dialer)
}
// NewSocks5Server returns a socks5 proxy server.
func NewSocks5Server(s string, dialer proxy.Dialer) (proxy.Server, error) {
return NewSOCKS5(s, dialer)
}
// ListenAndServe serves socks5 requests.
func (s *SOCKS5) ListenAndServe() {
go s.ListenAndServeUDP()
s.ListenAndServeTCP()
}
// ListenAndServeTCP .
func (s *SOCKS5) ListenAndServeTCP() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.F("[socks5] failed to listen on %s: %v", s.addr, err)
return
}
log.F("[socks5] listening TCP on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[socks5] failed to accept: %v", err)
continue
}
go s.ServeTCP(c)
}
}
// ServeTCP .
func (s *SOCKS5) ServeTCP(c net.Conn) {
defer c.Close()
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
tgt, err := s.handshake(c)
if err != nil {
// UDP: keep the connection until disconnect then free the UDP socket
if err == socks.Errors[9] {
buf := []byte{}
// block here
for {
_, err := c.Read(buf)
if err, ok := err.(net.Error); ok && err.Timeout() {
continue
}
// log.F("[socks5] servetcp udp associate end")
return
}
}
log.F("[socks5] failed to get target address: %v", err)
return
}
rc, err := s.dialer.Dial("tcp", tgt.String())
if err != nil {
log.F("[socks5] failed to connect to target: %v", err)
return
}
defer rc.Close()
log.F("[socks5] %s <-> %s", c.RemoteAddr(), tgt)
_, _, err = conn.Relay(c, rc)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
return // ignore i/o timeout
}
log.F("[socks5] relay error: %v", err)
}
}
// ListenAndServeUDP serves udp requests.
func (s *SOCKS5) ListenAndServeUDP() {
lc, err := net.ListenPacket("udp", s.addr)
if err != nil {
log.F("[socks5-udp] failed to listen on %s: %v", s.addr, err)
return
}
defer lc.Close()
log.F("[socks5-udp] listening UDP on %s", s.addr)
var nm sync.Map
buf := make([]byte, conn.UDPBufSize)
for {
c := NewPktConn(lc, nil, nil, true, nil)
n, raddr, err := c.ReadFrom(buf)
if err != nil {
log.F("[socks5-udp] remote read error: %v", err)
continue
}
var pc *PktConn
v, ok := nm.Load(raddr.String())
if !ok && v == nil {
if c.tgtAddr == nil {
log.F("[socks5-udp] can not get target address, not a valid request")
continue
}
lpc, nextHop, err := s.dialer.DialUDP("udp", c.tgtAddr.String())
if err != nil {
log.F("[socks5-udp] remote dial error: %v", err)
continue
}
pc = NewPktConn(lpc, nextHop, nil, false, nil)
nm.Store(raddr.String(), pc)
go func() {
conn.TimedCopy(c, raddr, pc, 2*time.Minute)
pc.Close()
nm.Delete(raddr.String())
}()
} else {
pc = v.(*PktConn)
}
_, err = pc.WriteTo(buf[:n], pc.writeAddr)
if err != nil {
log.F("[socks5-udp] remote write error: %v", err)
continue
}
log.F("[socks5-udp] %s <-> %s", raddr, c.tgtAddr)
}
}
// Addr returns forwarder's address
func (s *SOCKS5) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// NextDialer returns the next dialer
func (s *SOCKS5) NextDialer(dstAddr string) proxy.Dialer { return s.dialer.NextDialer(dstAddr) }
// Dial connects to the address addr on the network net via the SOCKS5 proxy.
func (s *SOCKS5) Dial(network, addr string) (net.Conn, error) {
switch network {
case "tcp", "tcp6", "tcp4":
default:
return nil, errors.New("[socks5]: no support for connection type " + network)
}
c, err := s.dialer.Dial(network, s.addr)
if err != nil {
log.F("dial to %s error: %s", s.addr, err)
return nil, err
}
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
if err := s.connect(c, addr); err != nil {
c.Close()
return nil, err
}
return c, nil
}
// DialUDP connects to the given address via the proxy.
func (s *SOCKS5) DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error) {
c, err := s.dialer.Dial("tcp", s.addr)
if err != nil {
log.F("[socks5] dialudp dial tcp to %s error: %s", s.addr, err)
return nil, nil, err
}
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
// send VER, NMETHODS, METHODS
c.Write([]byte{5, 1, 0})
buf := make([]byte, socks.MaxAddrLen)
// read VER METHOD
if _, err := io.ReadFull(c, buf[:2]); err != nil {
return nil, nil, err
}
dstAddr := socks.ParseAddr(addr)
// write VER CMD RSV ATYP DST.ADDR DST.PORT
c.Write(append([]byte{5, socks.CmdUDPAssociate, 0}, dstAddr...))
// read VER REP RSV ATYP BND.ADDR BND.PORT
if _, err := io.ReadFull(c, buf[:3]); err != nil {
return nil, nil, err
}
rep := buf[1]
if rep != 0 {
log.F("[socks5] server reply: %d, not succeeded", rep)
return nil, nil, errors.New("server connect failed")
}
uAddr, err := socks.ReadAddrBuf(c, buf)
if err != nil {
return nil, nil, err
}
pc, nextHop, err := s.dialer.DialUDP(network, uAddr.String())
if err != nil {
log.F("[socks5] dialudp to %s error: %s", uAddr.String(), err)
return nil, nil, err
}
pkc := NewPktConn(pc, nextHop, dstAddr, true, c)
return pkc, nextHop, err
}
// connect takes an existing connection to a socks5 proxy server,
// and commands the server to extend that connection to target,
// which must be a canonical address with a host and port.
func (s *SOCKS5) connect(conn net.Conn, target string) error {
host, portStr, err := net.SplitHostPort(target)
if err != nil {
return err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return errors.New("proxy: failed to parse port number: " + portStr)
}
if port < 1 || port > 0xffff {
return errors.New("proxy: port number out of range: " + portStr)
}
// the size here is just an estimate
buf := make([]byte, 0, 6+len(host))
buf = append(buf, Version)
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
buf = append(buf, 2 /* num auth methods */, socks.AuthNone, socks.AuthPassword)
} else {
buf = append(buf, 1 /* num auth methods */, socks.AuthNone)
}
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[0] != 5 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
}
if buf[1] == 0xff {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
}
if buf[1] == socks.AuthPassword {
buf = buf[:0]
buf = append(buf, 1 /* password protocol version */)
buf = append(buf, uint8(len(s.user)))
buf = append(buf, s.user...)
buf = append(buf, uint8(len(s.password)))
buf = append(buf, s.password...)
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[1] != 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
}
}
buf = buf[:0]
buf = append(buf, Version, socks.CmdConnect, 0 /* reserved */)
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
buf = append(buf, socks.ATypIP4)
ip = ip4
} else {
buf = append(buf, socks.ATypIP6)
}
buf = append(buf, ip...)
} else {
if len(host) > 255 {
return errors.New("proxy: destination hostname too long: " + host)
}
buf = append(buf, socks.ATypDomain)
buf = append(buf, byte(len(host)))
buf = append(buf, host...)
}
buf = append(buf, byte(port>>8), byte(port))
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
failure := "unknown error"
if int(buf[1]) < len(socks.Errors) {
failure = socks.Errors[buf[1]].Error()
}
if len(failure) > 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
}
bytesToDiscard := 0
switch buf[3] {
case socks.ATypIP4:
bytesToDiscard = net.IPv4len
case socks.ATypIP6:
bytesToDiscard = net.IPv6len
case socks.ATypDomain:
_, err := io.ReadFull(conn, buf[:1])
if err != nil {
return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
bytesToDiscard = int(buf[0])
default:
return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
}
if cap(buf) < bytesToDiscard {
buf = make([]byte, bytesToDiscard)
} else {
buf = buf[:bytesToDiscard]
}
if _, err := io.ReadFull(conn, buf); err != nil {
return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
// Also need to discard the port number
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
return nil
}
// Handshake fast-tracks SOCKS initialization to get target address to connect.
func (s *SOCKS5) handshake(rw io.ReadWriter) (socks.Addr, error) {
// Read RFC 1928 for request and reply structure and sizes.
buf := make([]byte, socks.MaxAddrLen)
// read VER, NMETHODS, METHODS
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return nil, err
}
nmethods := buf[1]
if _, err := io.ReadFull(rw, buf[:nmethods]); err != nil {
return nil, err
}
// write VER METHOD
if _, err := rw.Write([]byte{5, 0}); err != nil {
return nil, err
}
// read VER CMD RSV ATYP DST.ADDR DST.PORT
if _, err := io.ReadFull(rw, buf[:3]); err != nil {
return nil, err
}
cmd := buf[1]
addr, err := socks.ReadAddrBuf(rw, buf)
if err != nil {
return nil, err
}
switch cmd {
case socks.CmdConnect:
_, err = rw.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}) // SOCKS v5, reply succeeded
case socks.CmdUDPAssociate:
listenAddr := socks.ParseAddr(rw.(net.Conn).LocalAddr().String())
_, err = rw.Write(append([]byte{5, 0, 0}, listenAddr...)) // SOCKS v5, reply succeeded
if err != nil {
return nil, socks.Errors[7]
}
err = socks.Errors[9]
default:
return nil, socks.Errors[7]
}
return addr, err // skip VER, CMD, RSV fields
func init() {
proxy.AddUsage("socks5", `
Socks5 scheme:
socks5://[user:pass@]host:port
`)
}

View File

@ -1,25 +1,27 @@
package core
package cipher
import (
"crypto/md5"
"errors"
"net"
"sort"
"strings"
"github.com/shadowsocks/go-shadowsocks2/shadowaead"
"github.com/shadowsocks/go-shadowsocks2/shadowstream"
"github.com/nadoo/glider/proxy/ss/cipher/shadowaead"
"github.com/nadoo/glider/proxy/ss/cipher/shadowstream"
)
// Cipher interface.
type Cipher interface {
StreamConnCipher
PacketConnCipher
}
// StreamConnCipher is the stream connection cipher.
type StreamConnCipher interface {
StreamConn(net.Conn) net.Conn
}
// PacketConnCipher is the packet connection cipher.
type PacketConnCipher interface {
PacketConn(net.PacketConn) net.PacketConn
}
@ -36,6 +38,10 @@ var aeadList = map[string]struct {
"AEAD_AES_192_GCM": {24, shadowaead.AESGCM},
"AEAD_AES_256_GCM": {32, shadowaead.AESGCM},
"AEAD_CHACHA20_POLY1305": {32, shadowaead.Chacha20Poly1305},
// http://shadowsocks.org/en/spec/AEAD-Ciphers.html
// not listed in spec
"AEAD_XCHACHA20_POLY1305": {32, shadowaead.XChacha20Poly1305},
}
// List of stream ciphers: key size in bytes and constructor
@ -51,19 +57,11 @@ var streamList = map[string]struct {
"AES-256-CFB": {32, shadowstream.AESCFB},
"CHACHA20-IETF": {32, shadowstream.Chacha20IETF},
"XCHACHA20": {32, shadowstream.Xchacha20},
}
// ListCipher returns a list of available cipher names sorted alphabetically.
func ListCipher() []string {
var l []string
for k := range aeadList {
l = append(l, k)
}
for k := range streamList {
l = append(l, k)
}
sort.Strings(l)
return l
// http://shadowsocks.org/en/spec/Stream-Ciphers.html
// marked as "DO NOT USE"
"CHACHA20": {32, shadowstream.ChaCha20},
"RC4-MD5": {16, shadowstream.RC4MD5},
}
// PickCipher returns a Cipher of the given name. Derive key from password if given key is empty.
@ -71,14 +69,16 @@ func PickCipher(name string, key []byte, password string) (Cipher, error) {
name = strings.ToUpper(name)
switch name {
case "DUMMY":
case "DUMMY", "NONE":
return &dummy{}, nil
case "CHACHA20-IETF-POLY1305":
name = "AEAD_CHACHA20_POLY1305"
case "XCHACHA20-IETF-POLY1305":
name = "AEAD_XCHACHA20_POLY1305"
case "AES-128-GCM":
name = "AEAD_AES_128_GCM"
case "AES-196-GCM":
name = "AEAD_AES_196_GCM"
case "AES-192-GCM":
name = "AEAD_AES_192_GCM"
case "AES-256-GCM":
name = "AEAD_AES_256_GCM"
}
@ -123,7 +123,6 @@ func (ciph *streamCipher) PacketConn(c net.PacketConn) net.PacketConn {
}
// dummy cipher does not encrypt
type dummy struct{}
func (dummy) StreamConn(c net.Conn) net.Conn { return c }

View File

@ -11,6 +11,7 @@ import (
"golang.org/x/crypto/hkdf"
)
// Cipher generates a pair of stream ciphers for encryption and decryption.
type Cipher interface {
KeySize() int
SaltSize() int
@ -18,6 +19,7 @@ type Cipher interface {
Decrypter(salt []byte) (cipher.AEAD, error)
}
// KeySizeError is an error about the key size.
type KeySizeError int
func (e KeySizeError) Error() string {
@ -81,3 +83,12 @@ func Chacha20Poly1305(psk []byte) (Cipher, error) {
}
return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.New}, nil
}
// XChacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk)
// must be 32.
func XChacha20Poly1305(psk []byte) (Cipher, error) {
if len(psk) != chacha20poly1305.KeySize {
return nil, KeySizeError(chacha20poly1305.KeySize)
}
return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil
}

View File

@ -0,0 +1,67 @@
package shadowaead
import (
"crypto/rand"
"io"
"net"
)
type streamConn struct {
net.Conn
Cipher
r *reader
w *writer
}
// NewConn wraps a stream-oriented net.Conn with cipher.
func NewConn(c net.Conn, ciph Cipher) net.Conn { return &streamConn{Conn: c, Cipher: ciph} }
func (c *streamConn) initReader() error {
salt := make([]byte, c.SaltSize())
if _, err := io.ReadFull(c.Conn, salt); err != nil {
return err
}
aead, err := c.Decrypter(salt)
if err != nil {
return err
}
c.r = newReader(c.Conn, aead)
return nil
}
func (c *streamConn) Read(b []byte) (int, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.Read(b)
}
func (c *streamConn) initWriter() error {
salt := make([]byte, c.SaltSize())
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return err
}
aead, err := c.Encrypter(salt)
if err != nil {
return err
}
_, err = c.Conn.Write(salt)
if err != nil {
return err
}
c.w = newWriter(c.Conn, aead)
return nil
}
func (c *streamConn) Write(b []byte) (int, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.Write(b)
}

View File

@ -88,6 +88,10 @@ func (c *packetConn) ReadFrom(b []byte) (int, net.Addr, error) {
if err != nil {
return n, addr, err
}
b, err = Unpack(b, b[:n], c)
return len(b), addr, err
bb, err := Unpack(b[c.Cipher.SaltSize():], b[:n], c)
if err != nil {
return n, addr, err
}
copy(b, bb)
return len(bb), addr, err
}

View File

@ -0,0 +1,150 @@
// protocol:
// format: [encrypted payload length] [Overhead] [encrypted payload] [Overhead]
// sizes: 2 bytes, aead.Overhead() bytes, n bytes, aead.Overhead() bytes
// max(n): 0x3FFF
package shadowaead
import (
"crypto/cipher"
"encoding/binary"
"io"
"github.com/nadoo/glider/pkg/pool"
)
const (
lenSize = 2
// max payload size: 16383 from ss-libev aead.c: CHUNK_SIZE_MASK
maxPayload = 0x3FFF
// buf size, shoud enough to save lenSize + aead.Overhead() + maxPayload + aead.Overhead()
bufSize = 17 << 10
)
type writer struct {
io.Writer
cipher.AEAD
nonce [32]byte
}
// NewWriter wraps an io.Writer with AEAD encryption.
func NewWriter(w io.Writer, aead cipher.AEAD) io.Writer { return newWriter(w, aead) }
func newWriter(w io.Writer, aead cipher.AEAD) *writer {
return &writer{Writer: w, AEAD: aead}
}
// Write encrypts p and writes to the embedded io.Writer.
func (w *writer) Write(p []byte) (n int, err error) {
buf := pool.GetBuffer(bufSize)
defer pool.PutBuffer(buf)
nonce := w.nonce[:w.NonceSize()]
encLenSize := lenSize + w.Overhead()
for nw := maxPayload; n < len(p); n += nw {
if left := len(p) - n; left < maxPayload {
nw = left
}
binary.BigEndian.PutUint16(buf[:lenSize], uint16(nw))
w.Seal(buf[:0], nonce, buf[:lenSize], nil)
increment(nonce)
w.Seal(buf[:encLenSize], nonce, p[n:n+nw], nil)
increment(nonce)
if _, err = w.Writer.Write(buf[:encLenSize+nw+w.Overhead()]); err != nil {
return
}
}
return
}
type reader struct {
io.Reader
cipher.AEAD
nonce [32]byte
buf []byte
offset int
}
// NewReader wraps an io.Reader with AEAD decryption.
func NewReader(r io.Reader, aead cipher.AEAD) io.Reader { return newReader(r, aead) }
func newReader(r io.Reader, aead cipher.AEAD) *reader {
return &reader{Reader: r, AEAD: aead}
}
// NOTE: len(p) MUST >= max payload size + AEAD overhead.
func (r *reader) read(p []byte) (int, error) {
nonce := r.nonce[:r.NonceSize()]
// read encrypted lenSize + overhead
p = p[:lenSize+r.Overhead()]
if _, err := io.ReadFull(r.Reader, p); err != nil {
return 0, err
}
// decrypt lenSize
_, err := r.Open(p[:0], nonce, p, nil)
increment(nonce)
if err != nil {
return 0, err
}
// get payload size
size := int(binary.BigEndian.Uint16(p[:lenSize]))
// read encrypted payload + overhead
p = p[:size+r.Overhead()]
if _, err := io.ReadFull(r.Reader, p); err != nil {
return 0, err
}
// decrypt payload
_, err = r.Open(p[:0], nonce, p, nil)
increment(nonce)
if err != nil {
return 0, err
}
return size, nil
}
func (r *reader) Read(p []byte) (int, error) {
if r.buf == nil {
if len(p) >= maxPayload+r.Overhead() {
return r.read(p)
}
buf := pool.GetBuffer(bufSize)
n, err := r.read(buf)
if err != nil {
pool.PutBuffer(buf)
return 0, err
}
r.buf = buf[:n]
r.offset = 0
}
n := copy(p, r.buf[r.offset:])
r.offset += n
if r.offset == len(r.buf) {
pool.PutBuffer(r.buf)
r.buf = nil
}
return n, nil
}
// increment little-endian encoded unsigned integer b. Wrap around on overflow.
func increment(b []byte) {
for i := range b {
b[i]++
if b[i] != 0 {
return
}
}
}

View File

@ -3,9 +3,12 @@ package shadowstream
import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rc4"
"strconv"
"github.com/Yawning/chacha20"
"github.com/aead/chacha20"
"github.com/aead/chacha20/chacha"
)
// Cipher generates a pair of stream ciphers for encryption and decryption.
@ -15,6 +18,7 @@ type Cipher interface {
Decrypter(iv []byte) cipher.Stream
}
// KeySizeError is an error about the key size.
type KeySizeError int
func (e KeySizeError) Error() string {
@ -28,6 +32,7 @@ func (b *ctrStream) IVSize() int { return b.BlockSize() }
func (b *ctrStream) Decrypter(iv []byte) cipher.Stream { return b.Encrypter(iv) }
func (b *ctrStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCTR(b, iv) }
// AESCTR returns an aesctr cipher.
func AESCTR(key []byte) (Cipher, error) {
blk, err := aes.NewCipher(key)
if err != nil {
@ -43,6 +48,7 @@ func (b *cfbStream) IVSize() int { return b.BlockSize() }
func (b *cfbStream) Decrypter(iv []byte) cipher.Stream { return cipher.NewCFBDecrypter(b, iv) }
func (b *cfbStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCFBEncrypter(b, iv) }
// AESCFB returns an aescfb cipher.
func AESCFB(key []byte) (Cipher, error) {
blk, err := aes.NewCipher(key)
if err != nil {
@ -54,38 +60,84 @@ func AESCFB(key []byte) (Cipher, error) {
// IETF-variant of chacha20
type chacha20ietfkey []byte
func (k chacha20ietfkey) IVSize() int { return chacha20.INonceSize }
func (k chacha20ietfkey) IVSize() int { return chacha.INonceSize }
func (k chacha20ietfkey) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
func (k chacha20ietfkey) Encrypter(iv []byte) cipher.Stream {
ciph, err := chacha20.NewCipher(k, iv)
ciph, err := chacha20.NewCipher(iv, k)
if err != nil {
panic(err) // should never happen
}
return ciph
}
// Chacha20IETF returns a Chacha20IETF cipher.
func Chacha20IETF(key []byte) (Cipher, error) {
if len(key) != chacha20.KeySize {
return nil, KeySizeError(chacha20.KeySize)
if len(key) != chacha.KeySize {
return nil, KeySizeError(chacha.KeySize)
}
return chacha20ietfkey(key), nil
}
// xchacha20
type xchacha20key []byte
func (k xchacha20key) IVSize() int { return chacha20.XNonceSize }
func (k xchacha20key) IVSize() int { return chacha.XNonceSize }
func (k xchacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
func (k xchacha20key) Encrypter(iv []byte) cipher.Stream {
ciph, err := chacha20.NewCipher(k, iv)
ciph, err := chacha20.NewCipher(iv, k)
if err != nil {
panic(err) // should never happen
}
return ciph
}
// Xchacha20 returns a Xchacha20 cipher.
func Xchacha20(key []byte) (Cipher, error) {
if len(key) != chacha20.KeySize {
return nil, KeySizeError(chacha20.KeySize)
if len(key) != chacha.KeySize {
return nil, KeySizeError(chacha.KeySize)
}
return xchacha20key(key), nil
}
// chacah20
type chacha20key []byte
func (k chacha20key) IVSize() int { return chacha.NonceSize }
func (k chacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
func (k chacha20key) Encrypter(iv []byte) cipher.Stream {
ciph, err := chacha20.NewCipher(iv, k)
if err != nil {
panic(err) // should never happen
}
return ciph
}
// ChaCha20 returns a ChaCha20 cipher.
func ChaCha20(key []byte) (Cipher, error) {
if len(key) != chacha.KeySize {
return nil, KeySizeError(chacha.KeySize)
}
return chacha20key(key), nil
}
// rc4md5
type rc4Md5Key []byte
func (k rc4Md5Key) IVSize() int { return 16 }
func (k rc4Md5Key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
func (k rc4Md5Key) Encrypter(iv []byte) cipher.Stream {
h := md5.New()
h.Write([]byte(k))
h.Write(iv)
rc4key := h.Sum(nil)
ciph, err := rc4.NewCipher(rc4key)
if err != nil {
panic(err) // should never happen
}
return ciph
}
// RC4MD5 returns a RC4MD5 cipher.
func RC4MD5(key []byte) (Cipher, error) {
return rc4Md5Key(key), nil
}

View File

@ -0,0 +1,62 @@
package shadowstream
import (
"crypto/rand"
"io"
"net"
)
type conn struct {
net.Conn
Cipher
r *reader
w *writer
}
// NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption.
func NewConn(c net.Conn, ciph Cipher) net.Conn {
return &conn{Conn: c, Cipher: ciph}
}
func (c *conn) initReader() error {
if c.r == nil {
iv := make([]byte, c.IVSize())
if _, err := io.ReadFull(c.Conn, iv); err != nil {
return err
}
c.r = &reader{Reader: c.Conn, Stream: c.Decrypter(iv)}
}
return nil
}
func (c *conn) Read(b []byte) (int, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.Read(b)
}
func (c *conn) initWriter() error {
if c.w == nil {
iv := make([]byte, c.IVSize())
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return err
}
if _, err := c.Conn.Write(iv); err != nil {
return err
}
c.w = &writer{Writer: c.Conn, Stream: c.Encrypter(iv)}
}
return nil
}
func (c *conn) Write(b []byte) (int, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.Write(b)
}

View File

@ -71,6 +71,10 @@ func (c *packetConn) ReadFrom(b []byte) (int, net.Addr, error) {
if err != nil {
return n, addr, err
}
b, err = Unpack(b, b[:n], c.Cipher)
return len(b), addr, err
bb, err := Unpack(b[c.IVSize():], b[:n], c.Cipher)
if err != nil {
return n, addr, err
}
copy(b, bb)
return len(bb), addr, err
}

View File

@ -0,0 +1,55 @@
package shadowstream
import (
"crypto/cipher"
"io"
"github.com/nadoo/glider/pkg/pool"
)
const bufSize = 32 * 1024
type writer struct {
io.Writer
cipher.Stream
}
// NewWriter wraps an io.Writer with stream cipher encryption.
func NewWriter(w io.Writer, s cipher.Stream) io.Writer {
return &writer{Writer: w, Stream: s}
}
func (w *writer) Write(p []byte) (n int, err error) {
buf := pool.GetBuffer(bufSize)
defer pool.PutBuffer(buf)
for nw := 0; n < len(p) && err == nil; n += nw {
end := n + len(buf)
if end > len(p) {
end = len(p)
}
w.XORKeyStream(buf, p[n:end])
nw, err = w.Writer.Write(buf[:end-n])
}
return
}
type reader struct {
io.Reader
cipher.Stream
}
// NewReader wraps an io.Reader with stream cipher decryption.
func NewReader(r io.Reader, s cipher.Stream) io.Reader {
return &reader{Reader: r, Stream: s}
}
func (r *reader) Read(p []byte) (int, error) {
n, err := r.Reader.Read(p)
if err != nil {
return 0, err
}
p = p[:n]
r.XORKeyStream(p, p)
return n, nil
}

63
proxy/ss/client.go Normal file
View File

@ -0,0 +1,63 @@
package ss
import (
"errors"
"net"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/socks"
"github.com/nadoo/glider/proxy"
)
// NewSSDialer returns a ss proxy dialer.
func NewSSDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
return NewSS(s, d, nil)
}
// Addr returns forwarder's address.
func (s *SS) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// Dial connects to the address addr on the network net via the proxy.
func (s *SS) Dial(network, addr string) (net.Conn, error) {
target := socks.ParseAddr(addr)
if target == nil {
return nil, errors.New("[ss] unable to parse address: " + addr)
}
c, err := s.dialer.Dial("tcp", s.addr)
if err != nil {
log.F("[ss] dial to %s error: %s", s.addr, err)
return nil, err
}
c = s.StreamConn(c)
if _, err = c.Write(target); err != nil {
c.Close()
return nil, err
}
return c, err
}
// DialUDP connects to the given address via the proxy.
func (s *SS) DialUDP(network, addr string) (net.PacketConn, error) {
pc, err := s.dialer.DialUDP(network, s.addr)
if err != nil {
log.F("[ss] dialudp to %s error: %s", s.addr, err)
return nil, err
}
writeTo, err := net.ResolveUDPAddr("udp", s.addr)
if err != nil {
log.F("[ss] resolve addr error: %s", err)
return nil, err
}
pkc := NewPktConn(s.PacketConn(pc), writeTo, socks.ParseAddr(addr))
return pkc, nil
}

View File

@ -1,67 +1,84 @@
package ss
import (
"errors"
"net"
"github.com/nadoo/glider/common/socks"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/pkg/socks"
)
// PktConn .
type PktConn struct {
net.PacketConn
writeAddr net.Addr // write to and read from addr
tgtAddr socks.Addr
tgtHeader bool
writeTo net.Addr
target socks.Addr // if target is not nil, it may be a tunnel
}
// NewPktConn returns a PktConn
func NewPktConn(c net.PacketConn, writeAddr net.Addr, tgtAddr socks.Addr, tgtHeader bool) *PktConn {
pc := &PktConn{
PacketConn: c,
writeAddr: writeAddr,
tgtAddr: tgtAddr,
tgtHeader: tgtHeader}
return pc
// NewPktConn returns a PktConn, the writeAddr must be *net.UDPAddr or *net.UnixAddr.
func NewPktConn(c net.PacketConn, writeAddr net.Addr, targetAddr socks.Addr) *PktConn {
return &PktConn{PacketConn: c, writeTo: writeAddr, target: targetAddr}
}
// ReadFrom overrides the original function from net.PacketConn
// ReadFrom overrides the original function from net.PacketConn.
func (pc *PktConn) ReadFrom(b []byte) (int, net.Addr, error) {
if !pc.tgtHeader {
return pc.PacketConn.ReadFrom(b)
n, _, target, err := pc.readFrom(b)
return n, target, err
}
buf := make([]byte, len(b))
func (pc *PktConn) readFrom(b []byte) (int, net.Addr, net.Addr, error) {
buf := pool.GetBuffer(len(b))
defer pool.PutBuffer(buf)
n, raddr, err := pc.PacketConn.ReadFrom(buf)
if err != nil {
return n, raddr, err
return n, raddr, nil, err
}
tgtAddr := socks.SplitAddr(buf)
copy(b, buf[len(tgtAddr):])
//test
if pc.writeAddr == nil {
pc.writeAddr = raddr
tgtAddr := socks.SplitAddr(buf[:n])
if tgtAddr == nil {
return n, raddr, nil, errors.New("can not get target addr")
}
if pc.tgtAddr == nil {
pc.tgtAddr = tgtAddr
target, err := net.ResolveUDPAddr("udp", tgtAddr.String())
if err != nil {
return n, raddr, nil, errors.New("wrong target addr")
}
return n - len(tgtAddr), raddr, err
if pc.writeTo == nil {
pc.writeTo = raddr
}
if pc.target == nil {
pc.target = make([]byte, len(tgtAddr))
copy(pc.target, tgtAddr)
}
n = copy(b, buf[len(tgtAddr):n])
return n, raddr, target, err
}
// WriteTo overrides the original function from net.PacketConn
func (pc *PktConn) WriteTo(b []byte, addr net.Addr) (int, error) {
if !pc.tgtHeader {
return pc.PacketConn.WriteTo(b, addr)
target := pc.target
if addr != nil {
target = socks.ParseAddr(addr.String())
}
buf := make([]byte, len(pc.tgtAddr)+len(b))
copy(buf, pc.tgtAddr)
copy(buf[len(pc.tgtAddr):], b)
return pc.PacketConn.WriteTo(buf, pc.writeAddr)
if target == nil {
return 0, errors.New("invalid addr")
}
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
tgtLen, _ := buf.Write(target)
buf.Write(b)
n, err := pc.PacketConn.WriteTo(buf.Bytes(), pc.writeTo)
if n > tgtLen {
return n - tgtLen, err
}
return 0, err
}

178
proxy/ss/server.go Normal file
View File

@ -0,0 +1,178 @@
package ss
import (
"io"
"net"
"strings"
"sync"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/pkg/socks"
"github.com/nadoo/glider/proxy"
)
var nm sync.Map
// NewSSServer returns a ss proxy server.
func NewSSServer(s string, p proxy.Proxy) (proxy.Server, error) {
return NewSS(s, nil, p)
}
// ListenAndServe serves ss requests.
func (s *SS) ListenAndServe() {
go s.ListenAndServeUDP()
s.ListenAndServeTCP()
}
// ListenAndServeTCP serves tcp ss requests.
func (s *SS) ListenAndServeTCP() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.Fatalf("[ss] failed to listen on %s: %v", s.addr, err)
return
}
log.F("[ss] listening TCP on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[ss] failed to accept: %v", err)
continue
}
go s.Serve(c)
}
}
// Serve serves a connection.
func (s *SS) Serve(c net.Conn) {
defer c.Close()
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
sc := s.StreamConn(c)
tgt, err := socks.ReadAddr(sc)
if err != nil {
log.F("[ss] %s <-> target error: %v", c.RemoteAddr(), err)
proxy.Copy(io.Discard, c) // https://github.com/nadoo/glider/issues/180
return
}
dialer := s.proxy.NextDialer(tgt.String())
rc, err := dialer.Dial("tcp", tgt.String())
if err != nil {
log.F("[ss] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
return
}
defer rc.Close()
log.F("[ss] %s <-> %s via %s", c.RemoteAddr(), tgt, dialer.Addr())
if err = proxy.Relay(sc, rc); err != nil {
log.F("[ss] %s <-> %s via %s, relay error: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
// record remote conn failure only
if !strings.Contains(err.Error(), s.addr) {
s.proxy.Record(dialer, false)
}
}
}
// ListenAndServeUDP serves udp requests.
func (s *SS) ListenAndServeUDP() {
lc, err := net.ListenPacket("udp", s.addr)
if err != nil {
log.Fatalf("[ss] failed to listen on UDP %s: %v", s.addr, err)
return
}
defer lc.Close()
log.F("[ss] listening UDP on %s", s.addr)
s.ServePacket(lc)
}
// ServePacket implements proxy.PacketServer.
func (s *SS) ServePacket(pc net.PacketConn) {
lc := s.PacketConn(pc)
for {
c := NewPktConn(lc, nil, nil)
buf := pool.GetBuffer(proxy.UDPBufSize)
n, srcAddr, dstAddr, err := c.readFrom(buf)
if err != nil {
log.F("[ssu] remote read error: %v", err)
continue
}
var session *Session
sessionKey := srcAddr.String()
v, ok := nm.Load(sessionKey)
if !ok || v == nil {
session = newSession(sessionKey, srcAddr, dstAddr, c)
nm.Store(sessionKey, session)
go s.serveSession(session)
} else {
session = v.(*Session)
}
session.msgCh <- message{dstAddr, buf[:n]}
}
}
func (s *SS) serveSession(session *Session) {
dstPC, dialer, err := s.proxy.DialUDP("udp", session.dst.String())
if err != nil {
log.F("[ssu] remote dial error: %v", err)
nm.Delete(session.key)
return
}
defer dstPC.Close()
go func() {
proxy.CopyUDP(session.srcPC, nil, dstPC, 2*time.Minute, 5*time.Second)
nm.Delete(session.key)
close(session.finCh)
}()
log.F("[ssu] %s <-> %s via %s", session.src, session.dst, dialer.Addr())
for {
select {
case msg := <-session.msgCh:
_, err = dstPC.WriteTo(msg.msg, msg.dst)
if err != nil {
log.F("[ssu] writeTo %s error: %v", msg.dst, err)
}
pool.PutBuffer(msg.msg)
msg.msg = nil
case <-session.finCh:
return
}
}
}
type message struct {
dst net.Addr
msg []byte
}
// Session is a udp session
type Session struct {
key string
src net.Addr
dst net.Addr
srcPC *PktConn
msgCh chan message
finCh chan struct{}
}
func newSession(key string, src, dst net.Addr, srcPC *PktConn) *Session {
return &Session{key, src, dst, srcPC, make(chan message, 32), make(chan struct{})}
}

View File

@ -1,27 +1,20 @@
package ss
import (
"errors"
"net"
"net/url"
"strings"
"sync"
"time"
"github.com/shadowsocks/go-shadowsocks2/core"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/common/socks"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
"github.com/nadoo/glider/proxy/ss/cipher"
)
// SS .
// SS is a base ss struct.
type SS struct {
dialer proxy.Dialer
proxy proxy.Proxy
addr string
core.Cipher
cipher.Cipher
}
func init() {
@ -29,11 +22,11 @@ func init() {
proxy.RegisterServer("ss", NewSSServer)
}
// NewSS returns a shadowsocks proxy.
func NewSS(s string, dialer proxy.Dialer) (*SS, error) {
// NewSS returns a ss proxy.
func NewSS(s string, d proxy.Dialer, p proxy.Proxy) (*SS, error) {
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
log.F("[ss] parse err: %s", err)
return nil, err
}
@ -41,242 +34,33 @@ func NewSS(s string, dialer proxy.Dialer) (*SS, error) {
method := u.User.Username()
pass, _ := u.User.Password()
ciph, err := core.PickCipher(method, nil, pass)
ciph, err := cipher.PickCipher(method, nil, pass)
if err != nil {
log.Fatalf("[ss] PickCipher for '%s', error: %s", method, err)
}
p := &SS{
dialer: dialer,
ss := &SS{
dialer: d,
proxy: p,
addr: addr,
Cipher: ciph,
}
return p, nil
return ss, nil
}
// NewSSDialer returns a ss proxy dialer.
func NewSSDialer(s string, dialer proxy.Dialer) (proxy.Dialer, error) {
return NewSS(s, dialer)
}
// NewSSServer returns a ss proxy server.
func NewSSServer(s string, dialer proxy.Dialer) (proxy.Server, error) {
return NewSS(s, dialer)
}
// ListenAndServe serves ss requests.
func (s *SS) ListenAndServe() {
go s.ListenAndServeUDP()
s.ListenAndServeTCP()
}
// ListenAndServeTCP serves tcp ss requests.
func (s *SS) ListenAndServeTCP() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.F("[ss] failed to listen on %s: %v", s.addr, err)
return
}
log.F("[ss] listening TCP on %s", s.addr)
for {
c, err := l.Accept()
if err != nil {
log.F("[ss] failed to accept: %v", err)
continue
}
go s.ServeTCP(c)
}
}
// ServeTCP serves tcp ss requests.
func (s *SS) ServeTCP(c net.Conn) {
defer c.Close()
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
c = s.StreamConn(c)
tgt, err := socks.ReadAddr(c)
if err != nil {
log.F("[ss] failed to get target address: %v", err)
return
}
dialer := s.dialer.NextDialer(tgt.String())
// udp over tcp?
uot := socks.UoT(tgt[0])
if uot && dialer.Addr() == "DIRECT" {
rc, err := net.ListenPacket("udp", "")
if err != nil {
log.F("[ss-uottun] UDP remote listen error: %v", err)
}
defer rc.Close()
req := make([]byte, conn.UDPBufSize)
n, err := c.Read(req)
if err != nil {
log.F("[ss-uottun] error in ioutil.ReadAll: %s\n", err)
return
}
tgtAddr, _ := net.ResolveUDPAddr("udp", tgt.String())
rc.WriteTo(req[:n], tgtAddr)
buf := make([]byte, conn.UDPBufSize)
n, _, err = rc.ReadFrom(buf)
if err != nil {
log.F("[ss-uottun] read error: %v", err)
}
c.Write(buf[:n])
log.F("[ss] %s <-tcp-> %s - %s <-udp-> %s ", c.RemoteAddr(), c.LocalAddr(), rc.LocalAddr(), tgt)
return
}
network := "tcp"
if uot {
network = "udp"
}
rc, err := dialer.Dial(network, tgt.String())
if err != nil {
log.F("[ss] failed to connect to target: %v", err)
return
}
defer rc.Close()
log.F("[ss] %s <-> %s", c.RemoteAddr(), tgt)
_, _, err = conn.Relay(c, rc)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
return // ignore i/o timeout
}
log.F("[ss] relay error: %v", err)
}
}
// ListenAndServeUDP serves udp ss requests.
func (s *SS) ListenAndServeUDP() {
lc, err := net.ListenPacket("udp", s.addr)
if err != nil {
log.F("[ss-udp] failed to listen on %s: %v", s.addr, err)
return
}
defer lc.Close()
lc = s.PacketConn(lc)
log.F("[ss-udp] listening UDP on %s", s.addr)
var nm sync.Map
buf := make([]byte, conn.UDPBufSize)
for {
c := NewPktConn(lc, nil, nil, true)
n, raddr, err := c.ReadFrom(buf)
if err != nil {
log.F("[ss-udp] remote read error: %v", err)
continue
}
var pc *PktConn
v, ok := nm.Load(raddr.String())
if !ok && v == nil {
lpc, nextHop, err := s.dialer.DialUDP("udp", c.tgtAddr.String())
if err != nil {
log.F("[ss-udp] remote dial error: %v", err)
continue
}
pc = NewPktConn(lpc, nextHop, nil, false)
nm.Store(raddr.String(), pc)
go func() {
conn.TimedCopy(c, raddr, pc, 2*time.Minute)
pc.Close()
nm.Delete(raddr.String())
}()
} else {
pc = v.(*PktConn)
}
_, err = pc.WriteTo(buf[:n], pc.writeAddr)
if err != nil {
log.F("[ss-udp] remote write error: %v", err)
continue
}
log.F("[ss-udp] %s <-> %s", raddr, c.tgtAddr)
}
}
// ListCipher .
func ListCipher() string {
return strings.Join(core.ListCipher(), " ")
}
// Addr returns forwarder's address
func (s *SS) Addr() string {
if s.addr == "" {
return s.dialer.Addr()
}
return s.addr
}
// NextDialer returns the next dialer
func (s *SS) NextDialer(dstAddr string) proxy.Dialer { return s.dialer.NextDialer(dstAddr) }
// Dial connects to the address addr on the network net via the proxy.
func (s *SS) Dial(network, addr string) (net.Conn, error) {
target := socks.ParseAddr(addr)
if target == nil {
return nil, errors.New("[ss] unable to parse address: " + addr)
}
if network == "uot" {
target[0] = target[0] | 0x8
}
c, err := s.dialer.Dial("tcp", s.addr)
if err != nil {
log.F("[ss] dial to %s error: %s", s.addr, err)
return nil, err
}
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
c = s.StreamConn(c)
if _, err = c.Write(target); err != nil {
c.Close()
return nil, err
}
return c, err
}
// DialUDP connects to the given address via the proxy.
func (s *SS) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) {
pc, nextHop, err := s.dialer.DialUDP(network, s.addr)
if err != nil {
log.F("[ss] dialudp to %s error: %s", s.addr, err)
return nil, nil, err
}
pkc := NewPktConn(s.PacketConn(pc), nextHop, socks.ParseAddr(addr), true)
return pkc, nextHop, err
func init() {
proxy.AddUsage("ss", `
SS scheme:
ss://method:pass@host:port
Available methods for ss:
AEAD Ciphers:
AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AEAD_XCHACHA20_POLY1305
Stream Ciphers:
AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20 CHACHA20 RC4-MD5
Alias:
chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305, xchacha20-ietf-poly1305 = AEAD_XCHACHA20_POLY1305
Plain: NONE
`)
}

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