Compare commits

..

No commits in common. "main" and "v0.9.2" have entirely different histories.
main ... v0.9.2

172 changed files with 4565 additions and 14354 deletions

View File

@ -1,38 +0,0 @@
# 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"]

View File

@ -1,103 +1,39 @@
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
on: [push]
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Test
run: go test -v .
build:
name: Build
runs-on: ubuntu-latest
needs: [test]
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
uses: actions/setup-go@v1
with:
check-latest: true
go-version-file: "go.mod"
cache: true
go-version: 1.13
id: go
- name: Test
run: go test -v ./...
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- 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 }}
run: go build -v .

28
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Release
on:
create:
tags:
- v*
jobs:
release:
name: Release on GitHub
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code
uses: actions/checkout@v1
- name: Create release on GitHub
uses: goreleaser/goreleaser-action@v1
with:
version: latest
args: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,17 +0,0 @@
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"

8
.gitignore vendored
View File

@ -17,14 +17,6 @@
# custom
.idea
.vscode
.zed
.DS_Store
# dev test only
/dev/
dev*.go
*_test.go
dist

View File

@ -1,18 +1,30 @@
version: 2
# Make sure to check the documentation at http://goreleaser.com
# release:
# git tag -a v0.1.0 -m "v0.1.0"
# git push origin v0.1.0
# goreleaser release --skip-publish --rm-dist
# #git tag -d v0.1.0
# #git push origin --delete tag v0.1.0
# snapshot:
# goreleaser --snapshot --rm-dist
# https://goreleaser.com/customization/
before:
hooks:
- go mod tidy
# https://goreleaser.com/build/
builds:
- id: default
env:
- env:
- CGO_ENABLED=0
goos:
- windows
- linux
- darwin
- freebsd
goarch:
- 386
- amd64
@ -22,75 +34,32 @@ builds:
- mipsle
- mips64
- mips64le
- riscv64
goamd64:
- v1
- v3
goarm:
- 6
- 7
gomips:
- hardfloat
- softfloat
archives:
- id: default
builds:
- default
ignore:
- goos: darwin
goarch: 386
# https://goreleaser.com/archive/
archive:
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
wrap_in_directory: true
formats: tar.gz
format: tar.gz
format_overrides:
- goos: windows
formats: zip
format: zip
files:
- LICENSE
- README.md
- config/**/*
- systemd/*
# https://goreleaser.com/snapshots/
snapshot:
version_template: '{{ incpatch .Version }}-dev-{{.ShortCommit}}'
name_template: "dev@{{.ShortCommit}}"
# https://goreleaser.com/checksum/
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,14 +0,0 @@
# 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"]

607
README.md
View File

@ -1,12 +1,10 @@
# [glider](https://github.com/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 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)
[![Go Report Card](https://goreportcard.com/badge/github.com/nadoo/glider)](https://goreportcard.com/report/github.com/nadoo/glider)
[![GitHub release](https://img.shields.io/github/v/release/nadoo/glider.svg?include_prereleases)](https://github.com/nadoo/glider/releases)
[![Actions Status](https://github.com/nadoo/glider/workflows/Build/badge.svg)](https://github.com/nadoo/glider/actions)
glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset management features(like dnsmasq).
glider is a forward proxy with multiple protocols support, and also a dns forwarding server with ipset management features(like dnsmasq).
we can set up local listeners as proxy servers, and forward requests to internet via forwarders.
@ -17,274 +15,172 @@ we can set up local listeners as proxy servers, and forward requests to internet
```
## Features
- 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
## Protocols
Listen (local proxy server):
<details>
<summary>click to see details</summary>
- Socks5 proxy(tcp&udp)
- Http proxy(tcp)
- SS proxy(tcp&udp)
- Linux transparent proxy(iptables redirect)
- TCP tunnel
- UDP tunnel
- UDP over TCP tunnel
- TLS, use it together with above proxy protocols(tcp)
- Unix domain socket, use it together with above proxy protocols(tcp)
- KCP protocol, use it together with above proxy protocols(tcp)
|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
Forward (local proxy client/upstream proxy server):
</details>
- 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)
- Websocket, use it together with above proxy protocols(tcp)
- Unix domain socket, use it together with above proxy protocols(tcp)
- KCP protocol, use it together with above proxy protocols(tcp)
- Simple-Obfs, use it together with above proxy protocols(tcp)
DNS Forwarding Server (udp2tcp):
- DNS Over Proxy
- 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
- DNS cache
- 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
General:
- Http and socks5 on the same port
- Forwarder chain
- RR/HA/LHA/DH strategy for multiple forwarders
- Periodical proxy checking
- Rule proxy based on destinations: [Config Examples](config/examples)
- Send requests from specific ip/interface
TODO:
- [ ] IPv6 support in ipset manager
- [ ] Transparent UDP proxy (iptables tproxy)
- [ ] Performance tuning
- [ ] TUN/TAP device support
- [ ] SSH tunnel support (maybe)
## Install
- 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`
Binary:
- [https://github.com/nadoo/glider/releases](https://github.com/nadoo/glider/releases)
Go Get (requires **Go 1.13+** ):
```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
```
## Usage
#### Run
```bash
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
glider 0.9.0 usage:
-checkinterval int
fowarder check interval(seconds) (default 30)
-checklatencysamples int
use the average latency of the latest N checks (default 10)
proxy check interval(seconds) (default 30)
-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
proxy check timeout(seconds) (default 10)
-checkwebsite string
proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80 (default "www.apple.com")
-config string
config file path
-dialtimeout int
dial timeout(seconds) (default 3)
-dns string
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 address
-dnstimeout int
timeout value used in multiple dnsservers switch(seconds) (default 3)
-example
show usage examples
-forward value
forward url, see the URL section below
forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS]
-include value
include file
-interface string
source ip or source interface
-listen value
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)
listen url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS
-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
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)
forward strategy, default: rr (default "rr")
-verbose
verbose mode
URL:
proxy: SCHEME://[USER:PASS@][HOST]:PORT
chain: proxy,proxy[,proxy]...
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
tls: tls transport
ws: websocket transport
redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules)
redir6: redirect proxy(ipv6)
tcptun: tcp tunnel
udptun: udp tunnel
uottun: udp over tcp tunnel
unix: unix domain socket
kcp: kcp protocol
simple-obfs: simple-obfs protocol
reject: a virtual proxy which just reject connections
e.g. -listen socks5://:1080
-listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// (protocol chain)
Available schemes for different modes:
listen: mixed ss socks5 http redir redir6 tcptun udptun uottun tls unix kcp
forward: reject ss socks5 http ssr vmess tls ws unix kcp simple-obfs
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
@ -295,29 +191,28 @@ SS scheme:
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
Plain: DUMMY
--
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
--
VMess scheme:
vmess://[security:]uuid@host:port?alterID=num
Available securities for vmess:
none, aes-128-gcm, chacha20-poly1305
TLS client scheme:
tls://host:port[?serverName=SERVERNAME][&skipVerify=true][&cert=PATH][&alpn=proto1][&alpn=proto2]
tls://host:port[?skipVerify=true]
Proxy over tls client:
tls://host:port[?skipVerify=true][&serverName=SERVERNAME],scheme://
tls://host:port[?skipVerify=true],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]
tls://host:port?cert=PATH&key=PATH
Proxy over tls server:
tls://host:port?cert=PATH&key=PATH,scheme://
@ -325,66 +220,65 @@ Proxy over tls server:
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)
Websocket scheme:
ws://host:port[/path]
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)
Websocket with a specified proxy protocol:
ws://host:port[/path],scheme://
ws://host:port[/path],http://[user:pass@]
ws://host:port[/path],socks5://[user:pass@]
ws://host:port[/path],vmess://[security:]uuid@?alterID=num
TLS and Websocket with a specified proxy protocol:
tls://host:port[?skipVerify=true],ws://[@/path],scheme://
tls://host:port[?skipVerify=true],ws://[@/path],http://[user:pass@]
tls://host:port[?skipVerify=true],ws://[@/path],socks5://[user:pass@]
tls://host:port[?skipVerify=true],ws://[@/path],vmess://[security:]uuid@?alterID=num
--
Unix domain socket scheme:
unix://path
--
VLESS scheme:
vless://uuid@host:port[?fallback=127.0.0.1:80]
KCP scheme:
kcp://CRYPT:KEY@host:port[?dataShards=NUM&parityShards=NUM]
--
VMess scheme:
vmess://[security:]uuid@host:port[?alterID=num]
if alterID=0 or not set, VMessAEAD will be enabled
Available crypt types for KCP:
none, sm4, tea, xor, aes, aes-128, aes-192, blowfish, twofish, cast5, 3des, xtea, salsa20
Available security for vmess:
zero, none, aes-128-gcm, chacha20-poly1305
Simple-Obfs scheme:
simple-obfs://host:port[?type=TYPE&host=HOST&uri=URI&ua=UA]
--
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]
Available types for simple-obfs:
http, tls
Websocket server scheme:
ws://:port[/path][?host=HOST]
wss://:port[/path]?cert=PATH&key=PATH[?host=HOST]
DNS forwarding server:
dns=:53
dnsserver=8.8.8.8:53
dnsserver=1.1.1.1:53
dnsrecord=www.example.com/1.2.3.4
dnsrecord=www.example.com/2606:2800:220:1:248:1893:25c8:1946
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@]
Available forward strategies:
rr: Round Robin mode
ha: High Availability mode
lha: Latency based High Availability mode
dh: Destination Hashing mode
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
Forwarder option scheme: FORWARD_URL#OPTIONS
priority: set the priority of that forwarder, default:0
interface: set local interface or ip address used to connect remote server
-
Examples:
socks5://1.1.1.1:1080#priority=100
vmess://[security:]uuid@host:port?alterID=num#priority=200
vmess://[security:]uuid@host:port?alterID=num#priority=200&interface=192.168.1.99
vmess://[security:]uuid@host:port?alterID=num#priority=200&interface=eth0
--
VM socket scheme(linux only):
vsock://[CID]:port
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...
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.
@ -392,39 +286,53 @@ Examples:
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 ss://AEAD_CHACHA20_POLY1305:pass@:8443 -verbose
-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://user1:pass1@:1080 -verbose
-listen on :1080 as a socks5 proxy server, enable authentication.
glider -listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// -verbose
-protocol chain: listen on :443 as a https(http over tls) proxy server.
-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 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 :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 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 tcp://:80 -forward tcp://serverA:80
-tcp tunnel: listen on :80 and forward all requests to serverA:80.
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 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 redir://:1081 -forward "tls://1.1.1.1:443,vmess://security:uuid@?alterID=10"
-listen on :1081 as a transparent redirect server, forward all requests via remote tls+vmess 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.
glider -listen redir://:1081 -forward "ws://1.1.1.1:80,vmess://security:uuid@?alterID=10"
-listen on :1081 as a transparent redirect server, forward all requests via remote ws+vmess 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 -dns=:53 -dnsserver=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 -dnsrecord=www.example.com/1.2.3.4
-listen on :53 as dns server, forward dns requests to 8.8.8.8:53, return 1.2.3.4 when resolving www.example.com.
```
</details>
## Config
```bash
glider -config CONFIG_PATH
```
## Advanced Usage
- [ConfigFile](config)
- [glider.conf.example](config/glider.conf.example)
@ -433,68 +341,8 @@ glider -config CONFIG_PATH
- [transparent proxy with dnsmasq](config/examples/8.transparent_proxy_with_dnsmasq)
- [transparent proxy without dnsmasq](config/examples/9.transparent_proxy_without_dnsmasq)
## Service
- 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>
### Proxy & Protocol Chain
In glider, you can easily chain several proxy servers or protocols together, e.g:
- Chain proxy servers:
@ -505,19 +353,19 @@ glider -config CONFIG_PATH
- Chain protocols: https proxy (http over tls)
```bash
forward=tls://server.com:443,http://
forward=tls://1.1.1.1:443,http://
```
- Chain protocols: vmess over ws over tls
```bash
forward=tls://server.com:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
forward=tls://1.1.1.1: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
forward=socks5://1.1.1.1:1080,tls://2.2.2.2:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
```
- Chain protocols in listener: https proxy server
@ -526,17 +374,12 @@ glider -config CONFIG_PATH
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://
```
## Service
</details>
- systemd: [https://github.com/nadoo/glider/blob/master/systemd/](https://github.com/nadoo/glider/blob/master/systemd/)
## Links
- [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`
- [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

90
common/conn/conn.go Normal file
View File

@ -0,0 +1,90 @@
package conn
import (
"bufio"
"io"
"net"
"time"
)
// UDPBufSize is the size of udp buffer.
const UDPBufSize = 65536
// Conn is a base conn struct.
type Conn struct {
r *bufio.Reader
net.Conn
}
// NewConn returns a new conn.
func NewConn(c net.Conn) *Conn {
return &Conn{bufio.NewReader(c), c}
}
// Peek returns the next n bytes without advancing the reader.
func (c *Conn) Peek(n int) ([]byte, error) {
return c.r.Peek(n)
}
func (c *Conn) Read(p []byte) (int, error) {
return c.r.Read(p)
}
// Reader returns the internal bufio.Reader.
func (c *Conn) Reader() *bufio.Reader {
return c.r
}
// Relay relays between left and right.
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
}
// RelayUDP copys from src to dst at target with read timeout.
func RelayUDP(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 {
return ""
}
defer conn.Close()
return conn.LocalAddr().(*net.UDPAddr).IP.String()
}

25
common/log/log.go Normal file
View File

@ -0,0 +1,25 @@
package log
import (
stdlog "log"
)
func init() {
stdlog.SetFlags(stdlog.LstdFlags | stdlog.Lshortfile)
}
// 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...)
}

View File

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

306
conf.go Normal file
View File

@ -0,0 +1,306 @@
package main
import (
"fmt"
"log"
"os"
"path"
"github.com/nadoo/conflag"
"github.com/nadoo/glider/dns"
"github.com/nadoo/glider/rule"
"github.com/nadoo/glider/strategy"
)
var flag = conflag.New()
var conf struct {
Verbose bool
Listen []string
Forward []string
StrategyConfig strategy.Config
RuleFile []string
RulesDir string
DNS string
DNSConfig dns.Config
rules []*rule.Config
}
func confInit() {
flag.BoolVar(&conf.Verbose, "verbose", false, "verbose mode")
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.StringVar(&conf.StrategyConfig.Strategy, "strategy", "rr", "forward strategy, default: rr")
flag.StringVar(&conf.StrategyConfig.CheckWebSite, "checkwebsite", "www.apple.com", "proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80")
flag.IntVar(&conf.StrategyConfig.CheckInterval, "checkinterval", 30, "proxy check interval(seconds)")
flag.IntVar(&conf.StrategyConfig.CheckTimeout, "checktimeout", 10, "proxy check timeout(seconds)")
flag.IntVar(&conf.StrategyConfig.MaxFailures, "maxfailures", 3, "max failures to change forwarder status to disabled")
flag.StringVar(&conf.StrategyConfig.IntFace, "interface", "", "source ip or source interface")
flag.StringSliceUniqVar(&conf.RuleFile, "rulefile", nil, "rule file path")
flag.StringVar(&conf.RulesDir, "rules-dir", "", "rule file folder")
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.StringSliceUniqVar(&conf.DNSConfig.Records, "dnsrecord", nil, "custom dns record, format: domain/ip")
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 {
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() {
app := os.Args[0]
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "%s %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, " tls: tls transport\n")
fmt.Fprintf(os.Stderr, " ws: websocket transport\n")
fmt.Fprintf(os.Stderr, " redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules)\n")
fmt.Fprintf(os.Stderr, " redir6: redirect proxy(ipv6)\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, " unix: unix domain socket\n")
fmt.Fprintf(os.Stderr, " kcp: kcp protocol\n")
fmt.Fprintf(os.Stderr, " simple-obfs: simple-obfs protocol\n")
fmt.Fprintf(os.Stderr, " reject: a virtual proxy which just reject connections\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 redir6 tcptun udptun uottun tls unix kcp\n")
fmt.Fprintf(os.Stderr, " forward: reject ss socks5 http ssr vmess tls ws unix kcp simple-obfs\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 Ciphers:\n")
fmt.Fprintf(os.Stderr, " AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AEAD_XCHACHA20_POLY1305\n")
fmt.Fprintf(os.Stderr, " Stream Ciphers:\n")
fmt.Fprintf(os.Stderr, " AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20 CHACHA20 RC4-MD5\n")
fmt.Fprintf(os.Stderr, " Alias:\n")
fmt.Fprintf(os.Stderr, " chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305, xchacha20-ietf-poly1305 = AEAD_XCHACHA20_POLY1305\n")
fmt.Fprintf(os.Stderr, " Plain: DUMMY\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 securities for vmess:\n")
fmt.Fprintf(os.Stderr, " none, aes-128-gcm, chacha20-poly1305\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "TLS client scheme:\n")
fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true]\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Proxy over tls client:\n")
fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],scheme://\n")
fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],http://[user:pass@]\n")
fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],socks5://[user:pass@]\n")
fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],vmess://[security:]uuid@?alterID=num\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "TLS server scheme:\n")
fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Proxy over tls server:\n")
fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH,scheme://\n")
fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH,http://\n")
fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH,socks5://\n")
fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH,ss://method:pass@\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Websocket scheme:\n")
fmt.Fprintf(os.Stderr, " ws://host:port[/path]\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Websocket with a specified proxy protocol:\n")
fmt.Fprintf(os.Stderr, " ws://host:port[/path],scheme://\n")
fmt.Fprintf(os.Stderr, " ws://host:port[/path],http://[user:pass@]\n")
fmt.Fprintf(os.Stderr, " ws://host:port[/path],socks5://[user:pass@]\n")
fmt.Fprintf(os.Stderr, " ws://host:port[/path],vmess://[security:]uuid@?alterID=num\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "TLS and Websocket with a specified proxy protocol:\n")
fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],ws://[@/path],scheme://\n")
fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],ws://[@/path],http://[user:pass@]\n")
fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],ws://[@/path],socks5://[user:pass@]\n")
fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],ws://[@/path],vmess://[security:]uuid@?alterID=num\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Unix domain socket scheme:\n")
fmt.Fprintf(os.Stderr, " unix://path\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "KCP scheme:\n")
fmt.Fprintf(os.Stderr, " kcp://CRYPT:KEY@host:port[?dataShards=NUM&parityShards=NUM]\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Available crypt types for KCP:\n")
fmt.Fprintf(os.Stderr, " none, sm4, tea, xor, aes, aes-128, aes-192, blowfish, twofish, cast5, 3des, xtea, salsa20\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Simple-Obfs scheme:\n")
fmt.Fprintf(os.Stderr, " simple-obfs://host:port[?type=TYPE&host=HOST&uri=URI&ua=UA]\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Available types for simple-obfs:\n")
fmt.Fprintf(os.Stderr, " http, tls\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "DNS forwarding server:\n")
fmt.Fprintf(os.Stderr, " dns=:53\n")
fmt.Fprintf(os.Stderr, " dnsserver=8.8.8.8:53\n")
fmt.Fprintf(os.Stderr, " dnsserver=1.1.1.1:53\n")
fmt.Fprintf(os.Stderr, " dnsrecord=www.example.com/1.2.3.4\n")
fmt.Fprintf(os.Stderr, " dnsrecord=www.example.com/2606:2800:220:1:248:1893:25c8:1946\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, " lha: Latency based High Availability mode\n")
fmt.Fprintf(os.Stderr, " dh: Destination Hashing mode\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Forwarder option scheme: FORWARD_URL#OPTIONS\n")
fmt.Fprintf(os.Stderr, " priority: set the priority of that forwarder, default:0\n")
fmt.Fprintf(os.Stderr, " interface: set local interface or ip address used to connect remote server\n")
fmt.Fprintf(os.Stderr, " -\n")
fmt.Fprintf(os.Stderr, " Examples:\n")
fmt.Fprintf(os.Stderr, " socks5://1.1.1.1:1080#priority=100\n")
fmt.Fprintf(os.Stderr, " vmess://[security:]uuid@host:port?alterID=num#priority=200\n")
fmt.Fprintf(os.Stderr, " vmess://[security:]uuid@host:port?alterID=num#priority=200&interface=192.168.1.99\n")
fmt.Fprintf(os.Stderr, " vmess://[security:]uuid@host:port?alterID=num#priority=200&interface=eth0\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+" -listen :8443 -verbose\n")
fmt.Fprintf(os.Stderr, " -listen on :8443, serve as http/socks5 proxy on the same port, in verbose mode.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen ss://AEAD_CHACHA20_POLY1305:pass@:8443 -verbose\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://user1:pass1@:1080 -verbose\n")
fmt.Fprintf(os.Stderr, " -listen on :1080 as a socks5 proxy server, enable authentication.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// -verbose\n")
fmt.Fprintf(os.Stderr, " -listen on :443 as a https(http over tls) proxy server.\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://security:uuid@?alterID=10\"\n")
fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote tls+vmess server.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -forward \"ws://1.1.1.1:80,vmess://security:uuid@?alterID=10\"\n")
fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote ws+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 -dns=:53 -dnsserver=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")
fmt.Fprintf(os.Stderr, " "+app+" -verbose -dns=:53 -dnsserver=8.8.8.8:53 -dnsrecord=www.example.com/1.2.3.4\n")
fmt.Fprintf(os.Stderr, " -listen on :53 as dns server, forward dns requests to 8.8.8.8:53, return 1.2.3.4 when resolving www.example.com.\n")
fmt.Fprintf(os.Stderr, "\n")
}

254
config.go
View File

@ -1,254 +0,0 @@
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,15 +4,7 @@ Command:
```bash
glider -config glider.conf
```
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:
Config file, **just use the command line flag name as the key name**:
```bash
### glider config file
@ -31,11 +23,13 @@ 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 forward strategy
# multiple upstream proxies forwad strategy
strategy=rr
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
# 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
# check interval
checkinterval=30
@ -56,8 +50,8 @@ rules-dir=rules.d
#include=more.inc.conf
```
See:
- [glider.conf.example](glider.conf.example)
- [examples](examples)
- [glider.conf.example](config/glider.conf.example)
- [examples](config/examples)
## Rule File
Rule file, **same as the config file but specify forwarders based on destinations**:
@ -67,7 +61,7 @@ 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
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkwebsite=www.apple.com
checkinterval=30
# DNS SERVER for domains in this rule file

View File

@ -12,8 +12,10 @@ forward=http://1.1.1.1:8080
# High Availability mode: ha
strategy=rr
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
# 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
# check interval(seconds)
checkinterval=30

View File

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

View File

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

View File

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

View File

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

View File

@ -11,12 +11,13 @@ forward=http://1.1.1.1:8080
dns=:5353
dnsserver=8.8.8.8:53
strategy=rr
checkwebsite=www.apple.com
checkinterval=30
```
#### Create a ipset manually
```bash
ipset create myset hash:net
ipset create myset hash:ip
```
#### Config dnsmasq

View File

@ -12,6 +12,5 @@ dnsserver=8.8.8.8:53
strategy=rr
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkwebsite=www.apple.com
checkinterval=30

View File

@ -38,7 +38,8 @@ rules-dir=rules.d
forward=http://forwarder1:8080,socks5://forwarder2:1080
forward=http://1.1.1.1:8080
strategy=rr
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkwebsite=www.apple.com
checkinterval=30
# specify a different dns server(if need)
dnsserver=208.67.222.222:53
@ -82,20 +83,19 @@ Set server's nameserver to glider:
echo nameserver 127.0.0.1 > /etc/resolv.conf
```
#### Client settings
Use the linux server's ip as your gateway.
#### Client DNS settings
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 listens on the default dns port :53)
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 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.
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.
Destination Accessing:
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)
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.

View File

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

View File

@ -9,8 +9,7 @@ forward=http://forwarder2:8080,socks5://forwarder3:1080
# Round Robin mode: rr
# High Availability mode: ha
strategy=rr
# forwarder health check
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkwebsite=www.apple.com
checkinterval=30
# specify a different dns server(if need)

View File

@ -32,28 +32,28 @@ verbose=True
# different protocols.
# listen on 8443, serve as http/socks5 proxy on the same port.
# listen=:8443
listen=127.0.0.1:8443
listen=: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 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=socks5://:1080
# listen on 1081 as a linux transparent proxy server.
# listen=redir://:1081
# listen on 1082 as a linux transparent proxy server(tproxy).
# listen=tproxy://:1082
# 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 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
# 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
# http over tls (HTTPS proxy)
# listen=tls://:443?cert=crtFilePath&key=keyFilePath,http://
@ -62,28 +62,10 @@ listen=127.0.0.1:8443
# listen=tls://:443?cert=crtFilePath&key=keyFilePath,ss://AEAD_CHACHA20_POLY1305:pass@
# socks5 over unix domain socket
# listen=unix:///dev/shm/socket,socks5://
# socks5 over vm socket
# listen=vsock://:1234,socks5://
# listen=unix:///tmp/glider.socket,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
# listen=kcp://aes:key@127.0.0.1:8444?dataShards=10&parityShards=3,socks5://
# FORWARDERS
# ----------
@ -112,51 +94,36 @@ listen=127.0.0.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
# 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)
# vmess with none security
# forward=vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443?alterID=2
# vmess with aes-128-gcm security
# forward=vmess://aes-128-gcm: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
# forward=tls://1.1.1.1:443,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
# vmess over websocket
# forward=ws://1.1.1.1:80/path?host=server.com,vmess://chacha20-poly1305:5a146038-0b56-4e95-b1dc-5c6f5a32cd98@
# forward=ws://1.1.1.1:80/path,vmess://chacha20-poly1305:5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
# 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
# forward=tls://1.1.1.1:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
# forward=tls://1.1.1.1:443,ws://@/path,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
# ss over tls
# forward=tls://server.com:443,ss://AEAD_CHACHA20_POLY1305:pass@
# forward=tls://1.1.1.1: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@
# forward=kcp://aes:key@127.0.0.1:8444?dataShards=10&parityShards=3,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://
# forward=unix:///tmp/glider.socket,socks5://
# FORWARDER CHAIN
# ---------------
@ -175,55 +142,28 @@ listen=127.0.0.1:8443
# 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.
# 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
# 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
# check interval(seconds)
checkinterval=30
# timeout to set a forwarder to be disabled(seconds)
# check timeout(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
# ----------------
# 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
@ -243,55 +183,30 @@ 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.
# Specify the 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 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 MORE 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,7 +14,7 @@ forward=http://192.168.2.1:8080,socks5://192.168.2.2:1080
strategy=rr
# FORWARDER CHECK SETTINGS
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
checkwebsite=www.apple.com
checkinterval=30
# DNS SERVER for domains in this rule file
@ -26,7 +26,6 @@ dnsserver=208.67.222.222:53
# - 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

18
dev.go Normal file
View File

@ -0,0 +1,18 @@
//+build dev
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func init() {
go func() {
err := http.ListenAndServe(":6060", nil)
if err != nil {
fmt.Printf("Create pprof server error: %s\n", err)
}
}()
}

7
dev_linux.go Normal file
View File

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

View File

@ -5,123 +5,62 @@ import (
"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
}
// LongTTL is 50 years duration in seconds, used for none-expired items.
const LongTTL = 50 * 365 * 24 * 3600
// item is the struct of cache item.
type item struct {
key string
val []byte
exp int64
prev *item
next *item
value []byte
expire time.Time
}
// 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
// Cache is the struct of cache.
type Cache struct {
m map[string]*item
l sync.RWMutex
}
// 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
// NewCache returns a new cache.
func NewCache() (c *Cache) {
c = &Cache{m: make(map[string]*item)}
go func() {
for now := range time.Tick(time.Second) {
c.l.Lock()
for k, v := range c.m {
if now.After(v.expire) {
delete(c.m, k)
}
if it, ok := c.cache[k]; ok {
v = it.val
if it.exp < time.Now().Unix() {
expired = true
}
c.moveToHead(it)
c.l.Unlock()
}
}()
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()
// Len returns the length of cache.
func (c *Cache) Len() int {
return len(c.m)
}
if ttl == 0 {
c.store[k] = v
// Put an item into cache, invalid after ttl seconds.
func (c *Cache) Put(k string, v []byte, ttl int) {
if len(v) != 0 {
c.l.Lock()
it, ok := c.m[k]
if !ok {
it = &item{value: v}
c.m[k] = it
}
it.expire = time.Now().Add(time.Duration(ttl) * time.Second)
c.l.Unlock()
}
}
// Get an item from cache.
func (c *Cache) Get(k string) (v []byte) {
c.l.RLock()
if it, ok := c.m[k]; ok {
v = it.value
}
c.l.RUnlock()
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
}

View File

@ -1,22 +1,20 @@
package dns
import (
"bytes"
"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/common/log"
"github.com/nadoo/glider/proxy"
)
// AnswerHandler function handles the dns TypeA or TypeAAAA answer.
type AnswerHandler func(domain string, ip netip.Addr) error
// HandleFunc function handles the dns TypeA or TypeAAAA answer.
type HandleFunc func(Domain, ip string) error
// Config for dns.
type Config struct {
@ -26,73 +24,51 @@ type Config struct {
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
cache *Cache
config *Config
upStream *UPStream
upStreamMap map[string]*UPStream
handlers []AnswerHandler
upServers []string
upServerMap map[string][]string
handlers []HandleFunc
}
// NewClient returns a new dns client.
func NewClient(proxy proxy.Proxy, config *Config) (*Client, error) {
c := &Client{
proxy: proxy,
cache: NewLruCache(config.CacheSize),
cache: NewCache(),
config: config,
upStream: NewUPStream(config.Servers),
upStreamMap: make(map[string]*UPStream),
upServers: config.Servers,
upServerMap: make(map[string][]string),
}
// custom records
for _, record := range config.Records {
if err := c.AddRecord(record); err != nil {
log.F("[dns] add record '%s' error: %s", record, err)
}
c.AddRecord(record)
}
return c, nil
}
// Exchange handles request message and returns response message.
// TODO: optimize it
// reqBytes = reqLen + reqMsg
func (c *Client) Exchange(reqBytes []byte, clientAddr string, preferTCP bool) ([]byte, error) {
req, err := UnmarshalMessage(reqBytes)
req, err := UnmarshalMessage(reqBytes[2:])
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 {
v := c.cache.Get(getKey(req.Question))
if v != nil {
binary.BigEndian.PutUint16(v[2:4], req.ID)
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
}
}
@ -108,44 +84,20 @@ func (c *Client) Exchange(reqBytes []byte, clientAddr string, preferTCP bool) ([
return respBytes, nil
}
err = c.handleAnswer(respBytes, clientAddr, dnsServer, network, dialerAddr)
resp, err := UnmarshalMessage(respBytes[2:])
if err != nil {
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
ips := []string{}
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.IP != "" {
ips = append(ips, answer.IP)
}
if answer.TTL != 0 {
ttl = int(answer.TTL)
@ -153,7 +105,21 @@ func (c *Client) extractAnswer(resp *Message) ([]string, int) {
}
}
return ips, ttl
if ttl > c.config.MaxTTL {
ttl = c.config.MaxTTL
} else if ttl < c.config.MinTTL {
ttl = c.config.MinTTL
}
// add to cache only when there's a valid ip address
if len(ips) != 0 && ttl > 0 {
c.cache.Put(getKey(resp.Question), respBytes, ttl)
}
log.F("[dns] %s <-> %s(%s) via %s, type: %d, %s: %s",
clientAddr, dnsServer, network, dialerAddr, resp.Question.QTYPE, resp.Question.QNAME, strings.Join(ips, ","))
return respBytes, nil
}
// exchange choose a upstream dns server based on qname, communicate with it on the network.
@ -162,13 +128,12 @@ func (c *Client) exchange(qname string, reqBytes []byte, preferTCP bool) (
// use tcp to connect upstream server default
network = "tcp"
dialer := c.proxy.NextDialer(qname + ":0")
dialer := c.proxy.NextDialer(qname + ":53")
// if we are resolving a domain which uses a forwarder `REJECT`, then use `DIRECT` instead
// so we can resolve it correctly.
// if we are resolving the dialer's domain, then use Direct to avoid denpency loop
// TODO: dialer.Addr() == "REJECT", tricky
if dialer.Addr() == "REJECT" {
dialer = c.proxy.NextDialer("direct:0")
if strings.Contains(dialer.Addr(), qname) || dialer.Addr() == "REJECT" {
dialer = proxy.Default
}
// If client uses udp and no forwarders specified, use udp
@ -177,24 +142,18 @@ func (c *Client) exchange(qname string, reqBytes []byte, preferTCP bool) (
network = "udp"
}
ups := c.UpStream(qname)
server = ups.Server()
for range ups.Len() {
servers := c.GetServers(qname)
for _, server = range servers {
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
log.F("[dns] failed to connect to server %v: %v", server, err)
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":
@ -207,16 +166,7 @@ func (c *Client) exchange(qname string, reqBytes []byte, preferTCP bool) (
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)
log.F("[dns] failed to exchange with server %v: %v", server, err)
}
return server, network, dialer.Addr(), respBytes, err
@ -224,22 +174,23 @@ func (c *Client) exchange(qname string, reqBytes []byte, preferTCP bool) (
// 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 {
if _, err := rc.Write(reqBytes); err != nil {
log.F("[dns] failed to write req message: %v", err)
return nil, err
}
var respLen uint16
if err := binary.Read(rc, binary.BigEndian, &respLen); err != nil {
log.F("[dns] failed to read response length: %v", err)
return nil, err
}
respBytes := pool.GetBuffer(int(respLen))
_, err := io.ReadFull(rc, respBytes)
respBytes := make([]byte, respLen+2)
binary.BigEndian.PutUint16(respBytes[:2], respLen)
_, err := io.ReadFull(rc, respBytes[2:])
if err != nil {
log.F("[dns] error in read respMsg %s\n", err)
return nil, err
}
@ -248,97 +199,101 @@ func (c *Client) exchangeTCP(rc net.Conn, reqBytes []byte) ([]byte, error) {
// exchangeUDP exchange with server over udp.
func (c *Client) exchangeUDP(rc net.Conn, reqBytes []byte) ([]byte, error) {
if _, err := rc.Write(reqBytes); err != nil {
if _, err := rc.Write(reqBytes[2:]); err != nil {
log.F("[dns] failed to write req message: %v", err)
return nil, err
}
respBytes := pool.GetBuffer(UDPMaxLen)
n, err := rc.Read(respBytes)
reqBytes = make([]byte, 2+UDPMaxLen)
n, err := rc.Read(reqBytes[2:])
if err != nil {
return nil, err
}
binary.BigEndian.PutUint16(reqBytes[:2], uint16(n))
return respBytes[:n], nil
return reqBytes[:2+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)
func (c *Client) SetServers(domain string, servers ...string) {
c.upServerMap[domain] = append(c.upServerMap[domain], 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
// GetServers gets upstream dns servers for the given domain
func (c *Client) GetServers(domain string) []string {
domainParts := strings.Split(domain, ".")
length := len(domainParts)
for i := length - 1; i >= 0; i-- {
domain := strings.Join(domainParts[i:length], ".")
if servers, ok := c.upServerMap[domain]; ok {
return servers
}
}
return c.upStream
return c.upServers
}
// AddHandler adds a custom handler to handle the resolved result (A and AAAA).
func (c *Client) AddHandler(h AnswerHandler) {
func (c *Client) AddHandler(h HandleFunc) {
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)
r := strings.Split(record, "/")
domain, ip := r[0], r[1]
m, err := c.GenResponse(domain, ip)
if err != nil {
return err
}
c.cache.Set(qKey(m.Question), valCopy(wb.Bytes()), 0)
b, _ := m.Marshal()
var buf bytes.Buffer
binary.Write(&buf, binary.BigEndian, uint16(len(b)))
buf.Write(b)
c.cache.Put(getKey(m.Question), buf.Bytes(), LongTTL)
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
// GenResponse generates a dns response message for the given domain and ip address.
func (c *Client) GenResponse(domain string, ip string) (*Message, error) {
ipb := net.ParseIP(ip)
if ipb == nil {
return nil, errors.New("GenResponse: invalid ip format")
}
var qtype, rdlen uint16 = QTypeA, net.IPv4len
if addr.Is6() {
qtype, rdlen = QTypeAAAA, net.IPv6len
var rdata []byte
var qtype, rdlen uint16
if rdata = ipb.To4(); rdata != nil {
qtype = QTypeA
rdlen = net.IPv4len
} else {
qtype = QTypeAAAA
rdlen = net.IPv6len
rdata = ipb
}
m := NewMessage(0, ResponseMsg)
m := NewMessage(0, Response)
m.SetQuestion(NewQuestion(qtype, domain))
rr := &RR{NAME: domain, TYPE: qtype, CLASS: ClassINET,
TTL: ttl, RDLENGTH: rdlen, RDATA: addr.AsSlice()}
TTL: uint32(c.config.MinTTL), RDLENGTH: rdlen, RDATA: rdata}
m.AddAnswer(rr)
return m, nil
}
func qKey(q *Question) string {
return q.QNAME + "/" + strconv.FormatUint(uint64(q.QTYPE), 10)
func getKey(q *Question) string {
qtype := ""
switch q.QTYPE {
case QTypeA:
qtype = "A"
case QTypeAAAA:
qtype = "AAAA"
}
func valCopy(v []byte) (b []byte) {
if v != nil {
b = pool.GetBuffer(len(v))
copy(b, v)
}
return
return q.QNAME + "/" + qtype
}

View File

@ -4,29 +4,28 @@ import (
"bytes"
"encoding/binary"
"errors"
"io"
"math/rand/v2"
"net/netip"
"math/rand"
"net"
"strings"
)
// UDPMaxLen is the max size of udp dns request.
// https://www.dnsflagday.net/2020/
const UDPMaxLen = 1232
// 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.
const UDPMaxLen = 512
// HeaderLen is the length of dns msg header.
const HeaderLen = 12
// MsgType is the dns Message type.
type MsgType byte
// Message types.
// Message types
const (
QueryMsg MsgType = 0
ResponseMsg MsgType = 1
Query = 0
Response = 1
)
// Query types.
// Query types
const (
QTypeA uint16 = 1 //ipv4
QTypeAAAA uint16 = 28 ///ipv6
@ -35,8 +34,8 @@ const (
// ClassINET .
const ClassINET uint16 = 1
// Message format:
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1
// Message 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:
@ -64,7 +63,7 @@ type Message struct {
}
// NewMessage returns a new message.
func NewMessage(id uint16, msgType MsgType) *Message {
func NewMessage(id uint16, msgType int) *Message {
if id == 0 {
id = uint16(rand.Uint32())
}
@ -90,40 +89,32 @@ func (m *Message) AddAnswer(rr *RR) error {
// 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
}
var buf bytes.Buffer
// 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)
b, err := m.Header.Marshal()
if err != nil {
return
return nil, err
}
n += nn
buf.Write(b)
nn, err = m.Question.MarshalTo(w)
b, err = m.Question.Marshal()
if err != nil {
return
return nil, err
}
n += nn
buf.Write(b)
for _, answer := range m.Answers {
nn, err = answer.MarshalTo(w)
b, err := answer.Marshal()
if err != nil {
return
return nil, err
}
n += nn
buf.Write(b)
}
return
return buf.Bytes(), nil
}
// UnmarshalMessage unmarshals []bytes to Message.
@ -133,7 +124,8 @@ func UnmarshalMessage(b []byte) (*Message, error) {
}
m := &Message{unMarshaled: b}
if err := UnmarshalHeader(b[:HeaderLen], &m.Header); err != nil {
err := UnmarshalHeader(b[:HeaderLen], &m.Header)
if err != nil {
return nil, err
}
@ -146,7 +138,7 @@ func UnmarshalMessage(b []byte) (*Message, error) {
// resp answers
rrIdx := HeaderLen + qLen
for range int(m.Header.ANCOUNT) {
for i := 0; i < int(m.Header.ANCOUNT); i++ {
rr := &RR{}
rrLen, err := m.UnmarshalRR(rrIdx, rr)
if err != nil {
@ -162,8 +154,8 @@ func UnmarshalMessage(b []byte) (*Message, error) {
return m, nil
}
// Header format:
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.1
// Header format
// https://tools.ietf.org/html/rfc1035#section-4.1.1
// The header contains the following fields:
//
// 1 1 1 1 1 1
@ -181,6 +173,7 @@ func UnmarshalMessage(b []byte) (*Message, error) {
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ARCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
//
type Header struct {
ID uint16
Bits uint16
@ -191,7 +184,7 @@ type Header struct {
}
// SetMsgType sets the message type.
func (h *Header) SetMsgType(qr MsgType) {
func (h *Header) SetMsgType(qr int) {
h.Bits |= uint16(qr) << 15
}
@ -210,15 +203,16 @@ 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
// }
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)
// Marshal marshals header struct to []byte.
func (h *Header) Marshal() ([]byte, error) {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, h)
return buf.Bytes(), err
}
// UnmarshalHeader unmarshals []bytes to Header.
@ -241,8 +235,8 @@ func UnmarshalHeader(b []byte, h *Header) error {
return nil
}
// Question format:
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.2
// 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:
@ -273,24 +267,15 @@ func NewQuestion(qtype uint16, domain string) *Question {
}
}
// 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
}
// Marshal marshals Question struct to []byte.
func (q *Question) Marshal() ([]byte, error) {
var buf bytes.Buffer
if err = binary.Write(w, binary.BigEndian, q.QTYPE); err != nil {
return
}
n += 2
buf.Write(MarshalDomain(q.QNAME))
binary.Write(&buf, binary.BigEndian, q.QTYPE)
binary.Write(&buf, binary.BigEndian, q.QCLASS)
if err = binary.Write(w, binary.BigEndian, q.QCLASS); err != nil {
return
}
n += 2
return
return buf.Bytes(), nil
}
// UnmarshalQuestion unmarshals []bytes to Question.
@ -303,23 +288,21 @@ func (m *Message) UnmarshalQuestion(b []byte, q *Question) (n int, err error) {
return 0, errors.New("UnmarshalQuestion: not enough data")
}
sb := new(strings.Builder)
sb.Grow(32)
idx, err := m.UnmarshalDomainTo(sb, b)
domain, idx, err := m.UnmarshalDomain(b)
if err != nil {
return 0, err
}
q.QNAME = sb.String()
q.QNAME = domain
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
// RR format
// https://tools.ietf.org/html/rfc1035#section-3.2.1
// https://tools.ietf.org/html/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.
@ -353,48 +336,27 @@ type RR struct {
RDLENGTH uint16
RDATA []byte
IP netip.Addr
IP string
}
// NewRR returns a new dns rr.
func NewRR() *RR {
return &RR{}
rr := &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
}
// Marshal marshals RR struct to []byte.
func (rr *RR) Marshal() ([]byte, error) {
var buf bytes.Buffer
if err = binary.Write(w, binary.BigEndian, rr.TYPE); err != nil {
return
}
n += 2
buf.Write(MarshalDomain(rr.NAME))
binary.Write(&buf, binary.BigEndian, rr.TYPE)
binary.Write(&buf, binary.BigEndian, rr.CLASS)
binary.Write(&buf, binary.BigEndian, rr.TTL)
binary.Write(&buf, binary.BigEndian, rr.RDLENGTH)
buf.Write(rr.RDATA)
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
return buf.Bytes(), nil
}
// UnmarshalRR unmarshals []bytes to RR.
@ -405,14 +367,11 @@ func (m *Message) UnmarshalRR(start int, rr *RR) (n int, err error) {
p := m.unMarshaled[start:]
sb := new(strings.Builder)
sb.Grow(32)
n, err = m.UnmarshalDomainTo(sb, p)
domain, n, err := m.UnmarshalDomain(p)
if err != nil {
return 0, err
}
rr.NAME = sb.String()
rr.NAME = domain
if len(p) <= n+10 {
return 0, errors.New("UnmarshalRR: not enough data")
@ -430,9 +389,9 @@ func (m *Message) UnmarshalRR(start int, rr *RR) (n int, err error) {
rr.RDATA = p[n+10 : n+10+int(rr.RDLENGTH)]
if rr.TYPE == QTypeA {
rr.IP = netip.AddrFrom4(*(*[4]byte)(rr.RDATA[:4]))
rr.IP = net.IP(rr.RDATA[:net.IPv4len]).String()
} else if rr.TYPE == QTypeAAAA {
rr.IP = netip.AddrFrom16(*(*[16]byte)(rr.RDATA[:16]))
rr.IP = net.IP(rr.RDATA[:net.IPv6len]).String()
}
n = n + 10 + int(rr.RDLENGTH)
@ -440,88 +399,69 @@ func (m *Message) UnmarshalRR(start int, rr *RR) (n int, err error) {
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
// MarshalDomain marshals domain string struct to []byte.
func MarshalDomain(domain string) []byte {
var buf bytes.Buffer
for _, seg := range strings.Split(domain, ".") {
nn, err = w.Write([]byte{byte(len(seg))})
if err != nil {
return
binary.Write(&buf, binary.BigEndian, byte(len(seg)))
binary.Write(&buf, binary.BigEndian, []byte(seg))
}
n += nn
binary.Write(&buf, binary.BigEndian, byte(0x00))
nn, err = io.WriteString(w, seg)
if err != nil {
return
}
n += nn
return buf.Bytes()
}
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) {
// UnmarshalDomain gets domain from bytes.
func (m *Message) UnmarshalDomain(b []byte) (string, int, error) {
var idx, size int
var labels = []string{}
for len(b[idx:]) != 0 {
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.4
for {
// https://tools.ietf.org/html/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
label, err := m.UnmarshalDomainPoint(int(offset & 0x3FFF))
if err != nil {
return "", 0, err
}
labels = append(labels, label)
idx += 2
break
}
} else {
size = int(b[idx])
idx++
// root domain name
if size == 0 {
idx++
break
}
if size > 63 {
return 0, errors.New("UnmarshalDomainTo: label size larger than 63")
return "", 0, errors.New("UnmarshalDomain: label size larger than 63")
}
if idx+size > len(b) {
return 0, errors.New("UnmarshalDomainTo: label size larger than msg length")
if idx+size+1 > len(b) {
return "", 0, errors.New("UnmarshalDomain: label size larger than msg length")
}
if sb.Len() > 0 {
sb.WriteByte('.')
labels = append(labels, string(b[idx+1:idx+size+1]))
idx += (size + 1)
}
sb.Write(b[idx : idx+size])
idx += size
}
return idx, nil
domain := strings.Join(labels, ".")
return domain, idx, nil
}
// UnmarshalDomainPointTo gets domain from offset point to string builder.
func (m *Message) UnmarshalDomainPointTo(sb *strings.Builder, offset int) error {
// UnmarshalDomainPoint gets domain from offset point.
func (m *Message) UnmarshalDomainPoint(offset int) (string, error) {
if offset > len(m.unMarshaled) {
return errors.New("UnmarshalDomainPointTo: offset larger than msg length")
return "", errors.New("UnmarshalDomainPoint: offset larger than msg length")
}
_, err := m.UnmarshalDomainTo(sb, m.unMarshaled[offset:])
return err
domain, _, err := m.UnmarshalDomain(m.unMarshaled[offset:])
return domain, err
}

View File

@ -7,12 +7,11 @@ import (
"sync"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/proxy"
)
// conn timeout, in seconds.
// conn timeout, seconds
const timeout = 30
// Server is a dns server struct.
@ -25,15 +24,12 @@ type Server struct {
// 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
return s, err
}
// Start starts the dns forwarding server.
@ -49,46 +45,47 @@ func (s *Server) Start() {
// ListenAndServeUDP listen and serves on udp port.
func (s *Server) ListenAndServeUDP(wg *sync.WaitGroup) {
pc, err := net.ListenPacket("udp", s.addr)
c, 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()
defer c.Close()
log.F("[dns] listening UDP on %s", s.addr)
for {
reqBytes := pool.GetBuffer(UDPMaxLen)
n, caddr, err := pc.ReadFrom(reqBytes)
reqBytes := make([]byte, 2+UDPMaxLen)
n, caddr, err := c.ReadFrom(reqBytes[2:])
if err != nil {
log.F("[dns] local read error: %v", err)
pool.PutBuffer(reqBytes)
continue
}
go s.ServePacket(pc, caddr, reqBytes[:n])
reqLen := uint16(n)
if reqLen <= HeaderLen+2 {
log.F("[dns] not enough message data")
continue
}
binary.BigEndian.PutUint16(reqBytes[:2], reqLen)
go func() {
respBytes, err := s.Client.Exchange(reqBytes[:2+n], caddr.String(), false)
if err != nil {
log.F("[dns] error in exchange: %s", err)
return
}
_, err = c.WriteTo(respBytes[2:], caddr)
if err != nil {
log.F("[dns] error in local write: %s", err)
return
}
// 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.
@ -113,7 +110,7 @@ func (s *Server) ListenAndServeTCP(wg *sync.WaitGroup) {
}
}
// ServeTCP serves a dns tcp connection.
// ServeTCP serves a tcp connection.
func (s *Server) ServeTCP(c net.Conn) {
defer c.Close()
@ -125,27 +122,23 @@ func (s *Server) ServeTCP(c net.Conn) {
return
}
reqBytes := pool.GetBuffer(int(reqLen))
defer pool.PutBuffer(reqBytes)
_, err := io.ReadFull(c, reqBytes)
reqBytes := make([]byte, reqLen+2)
_, err := io.ReadFull(c, reqBytes[2:])
if err != nil {
log.F("[dns-tcp] error in read reqBytes %s", err)
return
}
binary.BigEndian.PutUint16(reqBytes[:2], reqLen)
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)
if err := binary.Write(c, binary.BigEndian, respBytes); err != nil {
log.F("[dns-tcp] error in local write respBytes: %s", err)
return
}
}

View File

@ -1,46 +0,0 @@
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)
}

View File

@ -1,27 +0,0 @@
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"
)

View File

@ -1,12 +0,0 @@
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"
)

44
go.mod
View File

@ -1,29 +1,27 @@
module github.com/nadoo/glider
go 1.24
go 1.13
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
github.com/klauspost/cpuid v1.2.1 // indirect
github.com/klauspost/reedsolomon v1.9.3 // indirect
github.com/nadoo/conflag v0.2.2
github.com/nadoo/go-shadowsocks2 v0.1.2
github.com/nadoo/shadowsocksR v0.1.0
github.com/pkg/errors v0.8.1 // indirect
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b // indirect
github.com/tjfoc/gmsm v1.0.1 // indirect
github.com/xtaci/kcp-go v5.4.11+incompatible
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 // indirect
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2 // indirect
)
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
)
// Replace dependency modules with local developing copy
// use `go list -m all` to confirm the final module used
// replace (
// github.com/nadoo/conflag => ../conflag
// github.com/nadoo/go-shadowsocks2 => ../go-shadowsocks2
// )

148
go.sum
View File

@ -1,129 +1,49 @@
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-camellia v0.0.0-20140412174459-3be6b3054dd1 h1:/5UddQ9I3CXetvBVN2ipRc209YUB0AMR8bufErftAxI=
github.com/dgryski/go-camellia v0.0.0-20140412174459-3be6b3054dd1/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/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY=
github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
github.com/nadoo/conflag v0.2.2 h1:xywuyaevdBnA3+4g9S11ng+Nby725WN1LXargWnAXpM=
github.com/nadoo/conflag v0.2.2/go.mod h1:dzFfDUpXdr2uS2oV+udpy5N2vfNOu/bFzjhX1WI52co=
github.com/nadoo/go-shadowsocks2 v0.1.2 h1:+tCSt65YAAMf24wj3tqv6a9oVBcqSGFYVsifBZwT9w8=
github.com/nadoo/go-shadowsocks2 v0.1.2/go.mod h1:/E2kSkS0mqF/e79wcAA0PezoWXk4CY9HldJlzwWtbwU=
github.com/nadoo/shadowsocksR v0.1.0 h1:sYPxZi0l8F1nxDDcckzb0DHXxhe0LNW5iSeohqPw6Fg=
github.com/nadoo/shadowsocksR v0.1.0/go.mod h1:nqcLRU7laARXdLLBrHP8odruT/6GIureicuRTs4R+RY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b h1:mnG1fcsIB1d/3vbkBak2MM0u+vhGhlQwpeimUi7QncM=
github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU=
github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc=
github.com/xtaci/kcp-go v5.4.11+incompatible h1:tJbtarpmOoOD74cZ41uvvF5Hyt1nvctHQCOxZ6ot5xw=
github.com/xtaci/kcp-go v5.4.11+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE=
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/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191010185427-af544f31c8ac h1:/b4NMZurYfBIQyRMqaPGMDeUrSW6gU7/7Hv6owY1Vjk=
golang.org/x/crypto v0.0.0-20191010185427-af544f31c8ac/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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/net v0.0.0-20191014212845-da9a3fd4c582 h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA=
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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/sys v0.0.0-20191009170203-06d7bd2c5f4f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2 h1:nq114VpM8lsSlP+lyUbANecYHYiFcSNFtqcBlxRV+gA=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=

View File

@ -1,78 +1,475 @@
// Apache License 2.0
// @mdlayher https://github.com/mdlayher/netlink
// Ref: https://github.com/vishvananda/netlink/blob/master/nl/nl_linux.go
package ipset
import (
"net/netip"
"bytes"
"encoding/binary"
"net"
"strings"
"sync"
"sync/atomic"
"syscall"
"unsafe"
"github.com/nadoo/ipset"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/rule"
)
// Manager struct.
// 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
// Manager struct
type Manager struct {
fd int
lsa syscall.SockaddrNetlink
domainSet sync.Map
}
// NewManager returns a Manager
func NewManager(rules []*rule.Config) (*Manager, error) {
if err := ipset.Init(); err != nil {
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 := &Manager{}
sets := make(map[string]struct{})
m := &Manager{fd: fd, lsa: lsa}
// create ipset
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")
if r.IPSet != "" {
CreateSet(fd, lsa, r.IPSet)
}
}
// init ipset
for _, r := range rules {
if r.IPSet != "" {
for _, domain := range r.Domain {
m.domainSet.Store(domain, r.IPSet)
}
for _, ip := range r.IP {
addToSet(r.IPSet, ip)
AddToSet(fd, lsa, r.IPSet, ip)
}
for _, cidr := range r.CIDR {
addToSet(r.IPSet, cidr)
AddToSet(fd, lsa, 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)
// AddDomainIP implements the DNSAnswerHandler function, used to update ipset according to domainSet rule
func (m *Manager) AddDomainIP(domain, ip string) error {
if ip != "" {
domainParts := strings.Split(domain, ".")
length := len(domainParts)
for i := length - 1; 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, ipset.(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)
// CreateSet create a ipset
func CreateSet(fd int, lsa syscall.SockaddrNetlink, setName string) {
if setName == "" {
return
}
func addAddrToSet(s string, ip netip.Addr) error {
if ip.Is4() {
return ipset.AddAddr(s, ip)
if len(setName) > IPSET_MAXNAMELEN {
log.Fatal("ipset: name too long")
}
return ipset.AddAddr(s+"6", ip)
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,10 +1,9 @@
//go:build !linux
// +build !linux
package ipset
import (
"errors"
"net/netip"
"github.com/nadoo/glider/rule"
)
@ -18,6 +17,6 @@ func NewManager(rules []*rule.Config) (*Manager, error) {
}
// AddDomainIP implements the DNSAnswerHandler function
func (m *Manager) AddDomainIP(domain string, ip netip.Addr) error {
func (m *Manager) AddDomainIP(domain, ip string) error {
return errors.New("ipset not supported on this os")
}

88
main.go
View File

@ -1,92 +1,92 @@
package main
import (
"context"
"net"
"fmt"
stdlog "log"
"os"
"os/signal"
"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/rule"
"github.com/nadoo/glider/service"
"github.com/nadoo/glider/strategy"
_ "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/reject"
_ "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/proxy/ws"
)
var (
version = "0.17.0"
config = parseConfig()
)
var version = "0.9.2"
func main() {
// read configs
confInit()
// setup a log func
log.F = func(f string, v ...interface{}) {
if conf.Verbose {
stdlog.Output(2, fmt.Sprintf(f, v...))
}
}
// global rule proxy
pxy := rule.NewProxy(config.Forwards, &config.Strategy, config.rules)
p := rule.NewProxy(conf.rules, strategy.NewProxy(conf.Forward, &conf.StrategyConfig))
// ipset manager
ipsetM, _ := ipset.NewManager(config.rules)
ipsetM, _ := ipset.NewManager(conf.rules)
// check and setup dns server
if config.DNS != "" {
d, err := dns.NewServer(config.DNS, pxy, &config.DNSConfig)
if conf.DNS != "" {
d, err := dns.NewServer(conf.DNS, p, &conf.DNSConfig)
if err != nil {
log.Fatal(err)
}
// rules
for _, r := range config.rules {
if len(r.DNSServers) > 0 {
// rule
for _, r := range conf.rules {
for _, domain := range r.Domain {
d.SetServers(domain, r.DNSServers)
if len(r.DNSServers) > 0 {
d.SetServers(domain, r.DNSServers...)
}
}
}
// add a handler to update proxy rules when a domain resolved
d.AddHandler(pxy.AddDomainIP)
d.AddHandler(p.AddDomainIP)
if ipsetM != nil {
d.AddHandler(ipsetM.AddDomainIP)
}
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()
p.Check()
// run proxy servers
for _, listen := range config.Listens {
local, err := proxy.ServerFromURL(listen, pxy)
// Proxy Servers
for _, listen := range conf.Listen {
local, err := proxy.ServerFromURL(listen, p)
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)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh

6
main_linux.go Normal file
View File

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

View File

@ -1,41 +0,0 @@
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...)
}

View File

@ -1,51 +0,0 @@
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))
}
}
}

View File

@ -1,24 +0,0 @@
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)
}

View File

@ -1,23 +0,0 @@
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)
}
}

View File

@ -1,106 +0,0 @@
// 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:])
}

View File

@ -1,128 +0,0 @@
// 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
}

View File

@ -1,621 +0,0 @@
// 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
}
}

View File

@ -1,56 +0,0 @@
// 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
}

View File

@ -1,615 +0,0 @@
// 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)
})
}

View File

@ -1,31 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,23 +0,0 @@
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

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

View File

@ -1,206 +0,0 @@
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,49 +3,34 @@ package proxy
import (
"errors"
"net"
"sort"
"net/url"
"strings"
)
var (
// ErrNotSupported indicates that the operation is not supported
ErrNotSupported = errors.New("not supported")
"github.com/nadoo/glider/common/log"
)
// Dialer is used to create connection.
type Dialer interface {
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
Dial(network, addr string) (c net.Conn, err error)
}
// UDPDialer is used to create udp PacketConn.
type UDPDialer interface {
// Addr is the dialer's addr
Addr() string
// DialUDP connects to the given address
DialUDP(network, addr string) (pc net.PacketConn, err error)
DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error)
}
// DialerCreator is a function to create dialers.
type DialerCreator func(s string, dialer Dialer) (Dialer, error)
var (
dialerCreators = make(map[string]DialerCreator)
dialerMap = make(map[string]DialerCreator)
)
// RegisterDialer is used to register a dialer.
func RegisterDialer(name string, c DialerCreator) {
dialerCreators[strings.ToLower(name)] = c
dialerMap[name] = c
}
// DialerFromURL calls the registered creator to create dialers.
@ -55,25 +40,16 @@ func DialerFromURL(s string, dialer Dialer) (Dialer, error) {
return nil, errors.New("DialerFromURL: dialer cannot be nil")
}
if !strings.Contains(s, "://") {
s = s + "://"
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
return nil, err
}
scheme := s[:strings.Index(s, ":")]
c, ok := dialerCreators[strings.ToLower(scheme)]
c, ok := dialerMap[strings.ToLower(u.Scheme)]
if ok {
return c(s, dialer)
}
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, " ")
return nil, errors.New("unknown scheme '" + u.Scheme + "'")
}

View File

@ -1,68 +1,54 @@
package proxy
import (
"context"
"errors"
"net"
"net/netip"
"time"
"github.com/nadoo/glider/pkg/sockopt"
"github.com/nadoo/glider/common/log"
)
// Direct proxy.
// Direct proxy
type Direct struct {
iface *net.Interface // interface specified by user
ip net.IP
dialTimeout time.Duration
relayTimeout time.Duration
}
func init() {
RegisterDialer("direct", NewDirectDialer)
// Default dialer
var Default = &Direct{}
// NewDirect returns a Direct dialer
func NewDirect(intface string) (*Direct, error) {
if intface == "" {
return &Direct{}, nil
}
// NewDirect returns a Direct dialer.
func NewDirect(intface string, dialTimeout, relayTimeout time.Duration) (*Direct, error) {
d := &Direct{dialTimeout: dialTimeout, relayTimeout: relayTimeout}
ip := net.ParseIP(intface)
if ip != nil {
return &Direct{ip: ip}, nil
}
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 &Direct{iface: iface}, nil
}
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.
// 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)
c, err = dial(network, addr, d.ip)
if err == nil {
return
}
}
for _, ip := range d.IFaceIPs() {
c, err = d.dial(network, addr, ip)
c, err = dial(network, addr, ip)
if err == nil {
d.ip = ip
break
@ -77,7 +63,11 @@ func (d *Direct) Dial(network, addr string) (c net.Conn, err error) {
return c, err
}
func (d *Direct) dial(network, addr string, localIP net.IP) (net.Conn, error) {
func dial(network, addr string, localIP net.IP) (net.Conn, error) {
if network == "uot" {
network = "udp"
}
var la net.Addr
switch network {
case "tcp":
@ -86,11 +76,7 @@ func (d *Direct) dial(network, addr string, localIP net.IP) (net.Conn, error) {
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))
}
dialer := &net.Dialer{LocalAddr: la}
c, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
@ -100,52 +86,37 @@ func (d *Direct) dial(network, addr string, localIP net.IP) (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.
func (d *Direct) DialUDP(network, addr string) (net.PacketConn, error) {
var la string
// DialUDP connects to the given address
func (d *Direct) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) {
// TODO: support specifying local interface
la := ""
if d.ip != nil {
la = net.JoinHostPort(d.ip.String(), "0")
la = d.ip.String() + ":0"
}
lc := &net.ListenConfig{}
if d.iface != nil {
lc.Control = sockopt.Control(sockopt.Bind(d.iface))
pc, err := net.ListenPacket(network, la)
if err != nil {
log.F("ListenPacket error: %s", err)
return nil, nil, err
}
return lc.ListenPacket(context.Background(), network, la)
uAddr, err := net.ResolveUDPAddr("udp", addr)
return pc, uAddr, err
}
// IFaceIPs returns ip addresses according to the specified interface.
// IFaceIPs returns ip addresses according to the specified interface
func (d *Direct) IFaceIPs() (ips []net.IP) {
ipNets, err := d.iface.Addrs()
ipnets, err := d.iface.Addrs()
if err != nil {
return
}
for _, ipNet := range ipNets {
ips = append(ips, ipNet.(*net.IPNet).IP) //!ip.IsLinkLocalUnicast()
for _, ipnet := range ipnets {
ips = append(ips, ipnet.(*net.IPNet).IP) //!ip.IsLinkLocalUnicast()
}
return
}
func init() {
AddUsage("direct", `
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
`)
}

View File

@ -1,13 +1,14 @@
package http
import (
"bytes"
"encoding/base64"
"errors"
"net"
"net/textproto"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/proxy"
)
@ -32,16 +33,14 @@ func (s *HTTP) Dial(network, addr string) (net.Conn, error) {
return nil, err
}
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
var buf bytes.Buffer
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")
buf.Write([]byte("Proxy-Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + "\r\n"))
}
// header ended
@ -51,7 +50,7 @@ func (s *HTTP) Dial(network, addr string) (net.Conn, error) {
return nil, err
}
c := proxy.NewConn(rc)
c := conn.NewConn(rc)
tpr := textproto.NewReader(c.Reader())
line, err := tpr.ReadLine()
if err != nil {
@ -77,6 +76,6 @@ func (s *HTTP) Dial(network, addr string) (net.Conn, error) {
}
// 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
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")
}

View File

@ -5,13 +5,13 @@
package http
import (
"bytes"
"encoding/base64"
"io"
"net/textproto"
"net/url"
"strings"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/proxy"
)
@ -81,17 +81,17 @@ func cleanHeaders(header textproto.MIMEHeader) {
header.Del("Upgrade")
}
func writeStartLine(w io.Writer, s1, s2, s3 string) {
io.WriteString(w, s1+" "+s2+" "+s3+"\r\n")
func writeStartLine(buf *bytes.Buffer, s1, s2, s3 string) {
buf.WriteString(s1 + " " + s2 + " " + s3 + "\r\n")
}
func writeHeaders(w io.Writer, header textproto.MIMEHeader) {
func writeHeaders(buf *bytes.Buffer, header textproto.MIMEHeader) {
for key, values := range header {
for _, v := range values {
io.WriteString(w, key+": "+v+"\r\n")
buf.WriteString(key + ": " + v + "\r\n")
}
}
io.WriteString(w, "\r\n")
buf.WriteString("\r\n")
}
func extractUserPass(auth string) (username, password string, ok bool) {
@ -112,10 +112,3 @@ func extractUserPass(auth string) (username, password string, ok bool) {
return s[:idx], s[idx+1:], true
}
func init() {
proxy.AddUsage("http", `
Http scheme:
http://[user:pass@]host:port
`)
}

View File

@ -3,16 +3,16 @@ package http
import (
"bufio"
"bytes"
"fmt"
"errors"
"net/textproto"
"net/url"
"strings"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/common/log"
)
// Methods are http methods from rfc.
// https://www.rfc-editor.org/rfc/rfc2616, http methods must be uppercase
// https://www.ietf.org/rfc/rfc2616.txt, http methods must be uppercase
var Methods = [...][]byte{
[]byte("GET"),
[]byte("POST"),
@ -46,7 +46,7 @@ func parseRequest(r *bufio.Reader) (*request, error) {
method, uri, proto, ok := parseStartLine(line)
if !ok {
return nil, fmt.Errorf("error in parseStartLine: %s", line)
return nil, errors.New("error in parseStartLine")
}
header, err := tpr.ReadMIMEHeader()
@ -60,26 +60,14 @@ func parseRequest(r *bufio.Reader) (*request, error) {
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, ":") {
var tgt = u.Host
if !strings.Contains(u.Host, ":") {
tgt += ":80"
}
@ -94,10 +82,12 @@ func parseRequest(r *bufio.Reader) (*request, error) {
if u.IsAbs() {
req.absuri = u.String()
u.Scheme, u.Host = "", ""
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
@ -109,12 +99,16 @@ func parseRequest(r *bufio.Reader) (*request, error) {
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) Marshal() []byte {
var buf bytes.Buffer
writeStartLine(&buf, r.method, r.ruri, r.proto)
writeHeaders(&buf, r.header)
return buf.Bytes()
}
func (r *request) WriteAbsBuf(buf *bytes.Buffer) {
writeStartLine(buf, r.method, r.absuri, r.proto)
writeHeaders(buf, r.header)
func (r *request) MarshalAbs() []byte {
var buf bytes.Buffer
writeStartLine(&buf, r.method, r.absuri, r.proto)
writeHeaders(&buf, r.header)
return buf.Bytes()
}

View File

@ -1,15 +1,16 @@
package http
import (
"bufio"
"bytes"
"fmt"
"io"
"net"
"net/textproto"
"strings"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/proxy"
)
@ -22,7 +23,7 @@ func NewHTTPServer(s string, p proxy.Proxy) (proxy.Server, error) {
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)
log.F("[http] failed to listen on %s: %v", s.addr, err)
return
}
defer l.Close()
@ -42,16 +43,20 @@ func (s *HTTP) ListenAndServe() {
// Serve serves a connection.
func (s *HTTP) Serve(cc net.Conn) {
if c, ok := cc.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
defer cc.Close()
c := proxy.NewConn(cc)
defer c.Close()
var c *conn.Conn
switch ccc := cc.(type) {
case *net.TCPConn:
ccc.SetKeepAlive(true)
c = conn.NewConn(ccc)
case *conn.Conn:
c = ccc
}
req, err := parseRequest(c.Reader())
if err != nil {
log.F("[http] can not parse request from %s, error: %v", c.RemoteAddr(), err)
log.F("[http] can not parse request from %s", c.RemoteAddr())
return
}
@ -64,11 +69,11 @@ func (s *HTTP) Serve(cc net.Conn) {
s.servRequest(req, c)
}
func (s *HTTP) servRequest(req *request, c *proxy.Conn) {
func (s *HTTP) servRequest(req *request, c *conn.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")
c.Write([]byte("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
}
@ -83,42 +88,38 @@ func (s *HTTP) servRequest(req *request, c *proxy.Conn) {
}
func (s *HTTP) servHTTPS(r *request, c net.Conn) {
rc, dialer, err := s.proxy.Dial("tcp", r.uri)
rc, p, 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)
c.Write([]byte(r.proto + " 502 ERROR\r\n\r\n"))
log.F("[http] %s <-> %s [c] via %s, error in dial: %v", c.RemoteAddr(), r.uri, p, err)
return
}
defer rc.Close()
io.WriteString(c, "HTTP/1.1 200 Connection established\r\n\r\n")
c.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
log.F("[http] %s <-> %s [c] via %s", c.RemoteAddr(), r.uri, dialer.Addr())
log.F("[http] %s <-> %s [c] via %s", c.RemoteAddr(), r.uri, p)
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)
_, _, err = conn.Relay(c, rc)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
return // ignore i/o timeout
}
log.F("[http] relay error: %v", err)
}
}
func (s *HTTP) servHTTP(req *request, c *proxy.Conn) {
rc, dialer, err := s.proxy.Dial("tcp", req.target)
func (s *HTTP) servHTTP(req *request, c *conn.Conn) {
rc, p, 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)
log.F("[http] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), req.target, p, 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())
_, err = rc.Write(req.Marshal())
if err != nil {
return
}
@ -126,15 +127,13 @@ func (s *HTTP) servHTTP(req *request, c *proxy.Conn) {
// 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)
io.Copy(rc, c)
rc.SetDeadline(time.Now())
c.SetDeadline(time.Now())
}
}()
r := pool.GetBufReader(rc)
defer pool.PutBufReader(r)
r := bufio.NewReader(rc)
tpr := textproto.NewReader(r)
line, err := tpr.ReadLine()
if err != nil {
@ -155,12 +154,12 @@ func (s *HTTP) servHTTP(req *request, c *proxy.Conn) {
header.Set("Proxy-Connection", "close")
header.Set("Connection", "close")
buf.Reset()
writeStartLine(buf, proto, code, status)
writeHeaders(buf, header)
var buf bytes.Buffer
writeStartLine(&buf, proto, code, status)
writeHeaders(&buf, header)
log.F("[http] %s <-> %s via %s", c.RemoteAddr(), req.target, dialer.Addr())
log.F("[http] %s <-> %s", c.RemoteAddr(), req.target)
c.Write(buf.Bytes())
proxy.Copy(c, r)
io.Copy(c, r)
}

View File

@ -3,16 +3,15 @@ package kcp
import (
"crypto/sha1"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"strings"
kcp "github.com/xtaci/kcp-go/v5"
kcp "github.com/xtaci/kcp-go"
"golang.org/x/crypto/pbkdf2"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/proxy"
)
@ -25,7 +24,6 @@ type KCP struct {
key string
crypt string
block kcp.BlockCrypt
mode string
dataShards int
parityShards int
@ -82,28 +80,14 @@ func NewKCP(s string, d proxy.Dialer, p proxy.Proxy) (*KCP, error) {
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 {
pass := pbkdf2.Key([]byte(k.key), []byte("kcp-go"), 4096, 32, sha1.New)
var block kcp.BlockCrypt
switch k.crypt {
case "sm4":
block, _ = kcp.NewSM4BlockCrypt(pass[:16])
case "tea":
@ -131,9 +115,13 @@ func block(crypt, key string) (block kcp.BlockCrypt, err error) {
case "salsa20":
block, _ = kcp.NewSalsa20BlockCrypt(pass)
default:
err = errors.New("unknown crypt type '" + crypt + "'")
return nil, errors.New("[kcp] unknown crypt type '" + k.crypt + "'")
}
return block, err
k.block = block
}
return k, nil
}
// NewKCPDialer returns a kcp proxy dialer.
@ -143,18 +131,23 @@ func NewKCPDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
// 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)
transport := strings.Split(s, ",")
// prepare transport listener
// TODO: check here
if len(transport) < 2 {
return nil, errors.New("[kcp] malformd listener:" + s)
}
k, err := NewKCP(transport[0], nil, p)
if err != nil {
return nil, err
}
if len(schemes) > 1 {
k.server, err = proxy.ServerFromURL(schemes[1], p)
k.server, err = proxy.ServerFromURL(transport[1], p)
if err != nil {
return nil, err
}
}
return k, nil
}
@ -163,7 +156,7 @@ func NewKCPServer(s string, p proxy.Proxy) (proxy.Server, error) {
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)
log.F("[kcp] failed to listen on %s: %v", s.addr, err)
return
}
defer l.Close()
@ -177,7 +170,13 @@ func (s *KCP) ListenAndServe() {
continue
}
s.setParams(c)
// TODO: change them to customizable later?
c.SetStreamMode(true)
c.SetWriteDelay(false)
c.SetNoDelay(0, 30, 2, 1)
c.SetWindowSize(1024, 1024)
c.SetMtu(1350)
c.SetACKNoDelay(true)
go s.Serve(c)
}
@ -185,30 +184,11 @@ func (s *KCP) ListenAndServe() {
// Serve serves connections.
func (s *KCP) Serve(c net.Conn) {
// we know the internal server will close the connection after serve
// defer c.Close()
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)
}
}
}
@ -229,7 +209,13 @@ func (s *KCP) Dial(network, addr string) (net.Conn, error) {
return nil, err
}
s.setParams(c)
// TODO: change them to customizable later?
c.SetStreamMode(true)
c.SetWriteDelay(false)
c.SetNoDelay(0, 30, 2, 1)
c.SetWindowSize(1024, 1024)
c.SetMtu(1350)
c.SetACKNoDelay(true)
c.SetDSCP(0)
c.SetReadBuffer(4194304)
@ -239,43 +225,6 @@ func (s *KCP) Dial(network, addr string) (net.Conn, error) {
}
// 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
`)
func (s *KCP) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) {
return nil, nil, errors.New("kcp client does not support udp now")
}

View File

@ -1,10 +1,12 @@
package mixed
import (
"bytes"
"net"
"net/url"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/proxy"
"github.com/nadoo/glider/proxy/http"
"github.com/nadoo/glider/proxy/socks5"
@ -60,11 +62,11 @@ func (m *Mixed) ListenAndServe() {
l, err := net.Listen("tcp", m.addr)
if err != nil {
log.Fatalf("[mixed] failed to listen on %s: %v", m.addr, err)
log.F("[mixed] failed to listen on %s: %v", m.addr, err)
return
}
log.F("[mixed] http & socks5 server listening TCP on %s", m.addr)
log.F("[mixed] listening TCP on %s", m.addr)
for {
c, err := l.Accept()
@ -79,12 +81,38 @@ func (m *Mixed) ListenAndServe() {
// Serve serves connections.
func (m *Mixed) Serve(c net.Conn) {
conn := proxy.NewConn(c)
if head, err := conn.Peek(1); err == nil {
defer c.Close()
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
cc := conn.NewConn(c)
head, err := cc.Peek(1)
if err != nil {
// log.F("[mixed] socks5 peek error: %s", err)
return
}
// check socks5, client send socksversion: 5 as the first byte
if head[0] == socks5.Version {
m.socks5Server.Serve(conn)
m.socks5Server.Serve(cc)
return
}
head, err = cc.Peek(8)
if err != nil {
log.F("[mixed] http peek error: %s", err)
return
}
for _, method := range http.Methods {
if bytes.HasPrefix(head, method) {
m.httpServer.Serve(cc)
return
}
}
m.httpServer.Serve(conn)
log.F("[mixed] unknown request from %s, ignored", c.RemoteAddr())
}

View File

@ -2,12 +2,11 @@ package obfs
import (
"bufio"
"bytes"
"crypto/rand"
"encoding/base64"
"io"
"net"
"github.com/nadoo/glider/pkg/pool"
)
// HTTPObfs struct
@ -43,19 +42,16 @@ func (p *HTTPObfs) NewConn(c net.Conn) (net.Conn, error) {
}
func (c *HTTPObfsConn) writeHeader() (int, error) {
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
buf := new(bytes.Buffer)
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)
p := make([]byte, 16)
rand.Read(p)
buf.WriteString("Sec-WebSocket-Key: " + base64.StdEncoding.EncodeToString(p) + "\r\n")
buf.WriteString("\r\n")

View File

@ -6,7 +6,7 @@ import (
"net"
"net/url"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/proxy"
)
@ -106,16 +106,6 @@ func (s *Obfs) Dial(network, addr string) (net.Conn, error) {
}
// 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
`)
func (s *Obfs) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) {
return nil, nil, errors.New("obfs client does not support udp now")
}

View File

@ -1,7 +1,8 @@
// https://www.rfc-editor.org/rfc/rfc5246
// https://www.ietf.org/rfc/rfc5246.txt
// https://golang.org/src/crypto/tls/handshake_messages.go
// NOTE:
// https://github.com/shadowsocks/simple-obfs/blob/master/src/obfs_tls.c
// 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.
@ -16,8 +17,6 @@ import (
"io"
"net"
"time"
"github.com/nadoo/glider/pkg/pool"
)
const (
@ -42,13 +41,18 @@ type TLSObfsConn struct {
net.Conn
reqSent bool
reader *bufio.Reader
buf [lenSize]byte
buf []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}
cc := &TLSObfsConn{
Conn: c,
TLSObfs: p,
buf: make([]byte, lenSize),
}
return cc, nil
}
@ -58,14 +62,14 @@ func (c *TLSObfsConn) Write(b []byte) (int, error) {
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)
end := i + chunkSize
if end > n {
end = n
}
buf := new(bytes.Buffer)
buf.Write([]byte{0x17, 0x03, 0x03})
binary.Write(buf, binary.BigEndian, uint16(len(b[i:end])))
buf.Write(b[i:end])
@ -120,18 +124,10 @@ func (c *TLSObfsConn) Read(b []byte) (int, error) {
}
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)
buf := new(bytes.Buffer)
// prepare extension & clientHello content
extension(b, c.obfsHost, bufExt)
clientHello(bufHello)
bufExt, bufHello := extension(b, c.obfsHost), clientHello()
// prepare lengths
extLen := bufExt.Len()
@ -166,7 +162,6 @@ func (c *TLSObfsConn) handshake(b []byte) (int, error) {
buf.Write(bufExt.Bytes())
_, err := c.Conn.Write(buf.Bytes())
if err != nil {
return 0, err
}
@ -174,7 +169,9 @@ func (c *TLSObfsConn) handshake(b []byte) (int, error) {
return len(b), nil
}
func clientHello(buf *bytes.Buffer) {
func clientHello() *bytes.Buffer {
buf := new(bytes.Buffer)
// Version: TLS 1.2 (0x0303)
buf.Write([]byte{0x03, 0x03})
@ -216,9 +213,13 @@ func clientHello(buf *bytes.Buffer) {
buf.WriteByte(0x01)
// Compression Methods (1 method)
buf.WriteByte(0x00)
return buf
}
func extension(b []byte, server string, buf *bytes.Buffer) {
func extension(b []byte, server string) *bytes.Buffer {
buf := new(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
@ -231,7 +232,7 @@ func extension(b []byte, server string, buf *bytes.Buffer) {
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)
buf.Write([]byte(server))
// https://github.com/shadowsocks/simple-obfs/blob/7659eeccf473aa41eb294e92c32f8f60a8747325/src/obfs_tls.c#L88
// Extension: ec_point_formats (len=4)
@ -262,4 +263,6 @@ func extension(b []byte, server string, buf *bytes.Buffer) {
// Extension: extended_master_secret (len=0)
buf.Write([]byte{0x00, 0x17}) // type
binary.Write(buf, binary.BigEndian, uint16(0)) // length
return buf
}

View File

@ -1,46 +1,15 @@
package proxy
import (
"net"
"strings"
)
import "net"
// Proxy is a dialer manager.
// 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)
Dial(network, addr string) (c net.Conn, proxy string, err error)
// DialUDP connects to the given address via the proxy.
DialUDP(network, addr string) (pc net.PacketConn, dialer UDPDialer, err error)
DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error)
// Get the dialer by dstAddr.
// 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
}

View File

@ -1,134 +0,0 @@
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,17 +1,28 @@
// 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/pkg/log"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/common/socks"
"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.
type RedirProxy struct {
proxy proxy.Proxy
@ -56,7 +67,7 @@ func NewRedir6Server(s string, p proxy.Proxy) (proxy.Server, error) {
func (s *RedirProxy) ListenAndServe() {
l, err := net.Listen("tcp", s.addr)
if err != nil {
log.Fatalf("[redir] failed to listen on %s: %v", s.addr, err)
log.F("[redir] failed to listen on %s: %v", s.addr, err)
return
}
@ -74,88 +85,99 @@ func (s *RedirProxy) ListenAndServe() {
}
// Serve serves connections.
func (s *RedirProxy) Serve(cc net.Conn) {
defer cc.Close()
func (s *RedirProxy) Serve(c net.Conn) {
defer c.Close()
c, ok := cc.(*net.TCPConn)
if !ok {
log.F("[redir] not a tcp connection, can not chain redir proxy")
return
if c, ok := c.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
c.SetKeepAlive(true)
tgtAddr, err := getOrigDst(c, s.ipv6)
tgt, err := getOrigDst(c, s.ipv6)
if err != nil {
log.F("[redir] failed to get target address: %v", err)
return
}
tgt := tgtAddr.String()
// loop request
if c.LocalAddr().String() == tgt {
if c.LocalAddr().String() == tgt.String() {
log.F("[redir] %s <-> %s, unallowed request to redir port", c.RemoteAddr(), tgt)
return
}
rc, dialer, err := s.proxy.Dial("tcp", tgt)
rc, p, err := s.proxy.Dial("tcp", tgt.String())
if err != nil {
log.F("[redir] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
log.F("[redir] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, p, err)
return
}
defer rc.Close()
log.F("[redir] %s <-> %s via %s", c.RemoteAddr(), tgt, dialer.Addr())
log.F("[redir] %s <-> %s via %s", c.RemoteAddr(), tgt, p)
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)
_, _, err = conn.Relay(c, rc)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
return // ignore i/o timeout
}
log.F("[redir] relay error: %v", err)
}
}
// Get the original destination of a TCP connection.
func getOrigDst(c *net.TCPConn, ipv6 bool) (netip.AddrPort, error) {
rc, err := c.SyscallConn()
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()
if err != nil {
return netip.AddrPort{}, err
return nil, err
}
var addr netip.AddrPort
rc.Control(func(fd uintptr) {
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
}
if ipv6 {
addr, err = getorigdstIPv6(fd)
} else {
addr, err = getorigdst(fd)
return getorigdstIPv6(fd)
}
})
return addr, err
return getorigdst(fd)
}
// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c
func getorigdst(fd uintptr) (netip.AddrPort, error) {
const _SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h
var raw syscall.RawSockaddrInet4
func getorigdst(fd uintptr) (socks.Addr, error) {
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 netip.AddrPort{}, err
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
}
// 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
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
}
// Call ipv6_getorigdst() from linux/net/ipv6/netfilter/nf_conntrack_l3proto_ipv6.c
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
func getorigdstIPv6(fd uintptr) (socks.Addr, error) {
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 netip.AddrPort{}, 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 nil, err
}
// 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
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
}

View File

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

View File

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

View File

@ -34,13 +34,6 @@ func (s *Reject) Dial(network, addr string) (net.Conn, error) {
}
// 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://
`)
func (s *Reject) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) {
return nil, nil, errors.New("REJECT")
}

View File

@ -3,11 +3,13 @@ package proxy
import (
"errors"
"net"
"sort"
"net/url"
"strings"
"github.com/nadoo/glider/common/log"
)
// Server interface.
// Server interface
type Server interface {
// ListenAndServe sets up a listener and serve on it
ListenAndServe()
@ -16,27 +18,22 @@ type Server interface {
Serve(c net.Conn)
}
// PacketServer interface.
type PacketServer interface {
ServePacket(pc net.PacketConn)
}
// ServerCreator is a function to create proxy servers.
// ServerCreator is a function to create proxy servers
type ServerCreator func(s string, proxy Proxy) (Server, error)
var (
serverCreators = make(map[string]ServerCreator)
serverMap = 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) {
serverCreators[strings.ToLower(name)] = c
serverMap[name] = c
}
// ServerFromURL calls the registered creator to create proxy servers.
// proxy can not be nil.
func ServerFromURL(s string, proxy Proxy) (Server, error) {
if proxy == nil {
// ServerFromURL calls the registered creator to create proxy servers
// dialer is the default upstream dialer so cannot be nil, we can use Default when calling this function
func ServerFromURL(s string, p Proxy) (Server, error) {
if p == nil {
return nil, errors.New("ServerFromURL: dialer cannot be nil")
}
@ -44,21 +41,16 @@ func ServerFromURL(s string, proxy Proxy) (Server, error) {
s = "mixed://" + s
}
scheme := s[:strings.Index(s, ":")]
c, ok := serverCreators[strings.ToLower(scheme)]
u, err := url.Parse(s)
if err != nil {
log.F("parse err: %s", err)
return nil, err
}
c, ok := serverMap[strings.ToLower(u.Scheme)]
if ok {
return c(s, proxy)
return c(s, p)
}
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, " ")
return nil, errors.New("unknown scheme '" + u.Scheme + "'")
}

View File

@ -1,79 +0,0 @@
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
}

View File

@ -1,119 +0,0 @@
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)
}
}
}

View File

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

View File

@ -1,195 +0,0 @@
// 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
`)
}

View File

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

View File

@ -1,299 +0,0 @@
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://www.rfc-editor.org/rfc/rfc1928
// https://tools.ietf.org/html/rfc1928
// socks5 client:
// https://github.com/golang/net/tree/master/proxy
@ -10,9 +10,17 @@
package socks5
import (
"errors"
"io"
"net"
"net/url"
"strconv"
"sync"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/common/conn"
"github.com/nadoo/glider/common/log"
"github.com/nadoo/glider/common/socks"
"github.com/nadoo/glider/proxy"
)
@ -28,7 +36,12 @@ type Socks5 struct {
password string
}
// NewSocks5 returns a Proxy that makes SOCKS v5 connections to the given address.
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. (RFC 1928)
func NewSocks5(s string, d proxy.Dialer, p proxy.Proxy) (*Socks5, error) {
u, err := url.Parse(s)
@ -52,9 +65,464 @@ func NewSocks5(s string, d proxy.Dialer, p proxy.Proxy) (*Socks5, error) {
return h, nil
}
func init() {
proxy.AddUsage("socks5", `
Socks5 scheme:
socks5://[user:pass@]host:port
`)
// NewSocks5Dialer returns a socks5 proxy dialer.
func NewSocks5Dialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
return NewSocks5(s, d, nil)
}
// 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.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.Serve(c)
}
}
// Serve serves a connection.
func (s *Socks5) Serve(cc net.Conn) {
defer cc.Close()
var c *conn.Conn
switch ccc := cc.(type) {
case *net.TCPConn:
ccc.SetKeepAlive(true)
c = conn.NewConn(ccc)
case *conn.Conn:
c = ccc
}
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 := make([]byte, 1)
// 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, p, err := s.proxy.Dial("tcp", tgt.String())
if err != nil {
log.F("[socks5] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, p, err)
return
}
defer rc.Close()
log.F("[socks5] %s <-> %s via %s", c.RemoteAddr(), tgt, p)
_, _, 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.proxy.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.RelayUDP(c, raddr, pc, 2*time.Minute)
pc.Close()
nm.Delete(raddr.String())
}()
log.F("[socks5-udp] %s <-> %s", raddr, c.tgtAddr)
} 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
}
// 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("[socks5]: 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 *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
}
// send VER, NMETHODS, METHODS
c.Write([]byte{Version, 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{Version, 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] != Version {
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 s.user != "" && s.password != "" {
_, err := rw.Write([]byte{Version, socks.AuthPassword})
if err != nil {
return nil, err
}
_, err = io.ReadFull(rw, buf[:2])
if err != nil {
return nil, err
}
// Get username
userLen := int(buf[1])
if userLen <= 0 {
rw.Write([]byte{1, 1})
return nil, errors.New("auth failed: wrong username length")
}
if _, err := io.ReadFull(rw, buf[:userLen]); err != nil {
return nil, errors.New("auth failed: cannot get username")
}
user := string(buf[:userLen])
// Get password
_, err = rw.Read(buf[:1])
if err != nil {
return nil, errors.New("auth failed: cannot get password len")
}
passLen := int(buf[0])
if passLen <= 0 {
rw.Write([]byte{1, 1})
return nil, errors.New("auth failed: wrong password length")
}
_, err = io.ReadFull(rw, 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 = rw.Write([]byte{1, 1})
if err != nil {
return nil, err
}
return nil, errors.New("auth failed, authinfo: " + user + ":" + pass)
}
// Response auth state
_, err = rw.Write([]byte{1, 0})
if err != nil {
return nil, err
}
} else if _, err := rw.Write([]byte{Version, socks.AuthNone}); 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
}

View File

@ -1,143 +0,0 @@
package cipher
import (
"crypto/md5"
"errors"
"net"
"strings"
"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
}
// ErrCipherNotSupported occurs when a cipher is not supported (likely because of security concerns).
var ErrCipherNotSupported = errors.New("cipher not supported")
// List of AEAD ciphers: key size in bytes and constructor
var aeadList = map[string]struct {
KeySize int
New func([]byte) (shadowaead.Cipher, error)
}{
"AEAD_AES_128_GCM": {16, shadowaead.AESGCM},
"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
var streamList = map[string]struct {
KeySize int
New func(key []byte) (shadowstream.Cipher, error)
}{
"AES-128-CTR": {16, shadowstream.AESCTR},
"AES-192-CTR": {24, shadowstream.AESCTR},
"AES-256-CTR": {32, shadowstream.AESCTR},
"AES-128-CFB": {16, shadowstream.AESCFB},
"AES-192-CFB": {24, shadowstream.AESCFB},
"AES-256-CFB": {32, shadowstream.AESCFB},
"CHACHA20-IETF": {32, shadowstream.Chacha20IETF},
"XCHACHA20": {32, shadowstream.Xchacha20},
// 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.
func PickCipher(name string, key []byte, password string) (Cipher, error) {
name = strings.ToUpper(name)
switch name {
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-192-GCM":
name = "AEAD_AES_192_GCM"
case "AES-256-GCM":
name = "AEAD_AES_256_GCM"
}
if choice, ok := aeadList[name]; ok {
if len(key) == 0 {
key = kdf(password, choice.KeySize)
}
if len(key) != choice.KeySize {
return nil, shadowaead.KeySizeError(choice.KeySize)
}
aead, err := choice.New(key)
return &aeadCipher{aead}, err
}
if choice, ok := streamList[name]; ok {
if len(key) == 0 {
key = kdf(password, choice.KeySize)
}
if len(key) != choice.KeySize {
return nil, shadowstream.KeySizeError(choice.KeySize)
}
ciph, err := choice.New(key)
return &streamCipher{ciph}, err
}
return nil, ErrCipherNotSupported
}
type aeadCipher struct{ shadowaead.Cipher }
func (aead *aeadCipher) StreamConn(c net.Conn) net.Conn { return shadowaead.NewConn(c, aead) }
func (aead *aeadCipher) PacketConn(c net.PacketConn) net.PacketConn {
return shadowaead.NewPacketConn(c, aead)
}
type streamCipher struct{ shadowstream.Cipher }
func (ciph *streamCipher) StreamConn(c net.Conn) net.Conn { return shadowstream.NewConn(c, ciph) }
func (ciph *streamCipher) PacketConn(c net.PacketConn) net.PacketConn {
return shadowstream.NewPacketConn(c, ciph)
}
// dummy cipher does not encrypt
type dummy struct{}
func (dummy) StreamConn(c net.Conn) net.Conn { return c }
func (dummy) PacketConn(c net.PacketConn) net.PacketConn { return c }
// key-derivation function from original Shadowsocks
func kdf(password string, keyLen int) []byte {
var b, prev []byte
h := md5.New()
for len(b) < keyLen {
h.Write(prev)
h.Write([]byte(password))
b = h.Sum(b)
prev = b[len(b)-h.Size():]
h.Reset()
}
return b[:keyLen]
}

View File

@ -1,94 +0,0 @@
package shadowaead
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha1"
"io"
"strconv"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
)
// Cipher generates a pair of stream ciphers for encryption and decryption.
type Cipher interface {
KeySize() int
SaltSize() int
Encrypter(salt []byte) (cipher.AEAD, error)
Decrypter(salt []byte) (cipher.AEAD, error)
}
// KeySizeError is an error about the key size.
type KeySizeError int
func (e KeySizeError) Error() string {
return "key size error: need " + strconv.Itoa(int(e)) + " bytes"
}
func hkdfSHA1(secret, salt, info, outkey []byte) {
r := hkdf.New(sha1.New, secret, salt, info)
if _, err := io.ReadFull(r, outkey); err != nil {
panic(err) // should never happen
}
}
type metaCipher struct {
psk []byte
makeAEAD func(key []byte) (cipher.AEAD, error)
}
func (a *metaCipher) KeySize() int { return len(a.psk) }
func (a *metaCipher) SaltSize() int {
if ks := a.KeySize(); ks > 16 {
return ks
}
return 16
}
func (a *metaCipher) Encrypter(salt []byte) (cipher.AEAD, error) {
subkey := make([]byte, a.KeySize())
hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey)
return a.makeAEAD(subkey)
}
func (a *metaCipher) Decrypter(salt []byte) (cipher.AEAD, error) {
subkey := make([]byte, a.KeySize())
hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey)
return a.makeAEAD(subkey)
}
func aesGCM(key []byte) (cipher.AEAD, error) {
blk, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(blk)
}
// AESGCM creates a new Cipher with a pre-shared key. len(psk) must be
// one of 16, 24, or 32 to select AES-128/196/256-GCM.
func AESGCM(psk []byte) (Cipher, error) {
switch l := len(psk); l {
case 16, 24, 32: // AES 128/196/256
default:
return nil, aes.KeySizeError(l)
}
return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil
}
// Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk)
// must be 32.
func Chacha20Poly1305(psk []byte) (Cipher, error) {
if len(psk) != chacha20poly1305.KeySize {
return nil, KeySizeError(chacha20poly1305.KeySize)
}
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

@ -1,67 +0,0 @@
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

@ -1,97 +0,0 @@
package shadowaead
import (
"crypto/rand"
"errors"
"io"
"net"
"sync"
)
// ErrShortPacket means that the packet is too short for a valid encrypted packet.
var ErrShortPacket = errors.New("short packet")
var _zerononce [128]byte // read-only. 128 bytes is more than enough.
// Pack encrypts plaintext using Cipher with a randomly generated salt and
// returns a slice of dst containing the encrypted packet and any error occurred.
// Ensure len(dst) >= ciph.SaltSize() + len(plaintext) + aead.Overhead().
func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) {
saltSize := ciph.SaltSize()
salt := dst[:saltSize]
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return nil, err
}
aead, err := ciph.Encrypter(salt)
if err != nil {
return nil, err
}
if len(dst) < saltSize+len(plaintext)+aead.Overhead() {
return nil, io.ErrShortBuffer
}
b := aead.Seal(dst[saltSize:saltSize], _zerononce[:aead.NonceSize()], plaintext, nil)
return dst[:saltSize+len(b)], nil
}
// Unpack decrypts pkt using Cipher and returns a slice of dst containing the decrypted payload and any error occurred.
// Ensure len(dst) >= len(pkt) - aead.SaltSize() - aead.Overhead().
func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) {
saltSize := ciph.SaltSize()
if len(pkt) < saltSize {
return nil, ErrShortPacket
}
salt := pkt[:saltSize]
aead, err := ciph.Decrypter(salt)
if err != nil {
return nil, err
}
if len(pkt) < saltSize+aead.Overhead() {
return nil, ErrShortPacket
}
if saltSize+len(dst)+aead.Overhead() < len(pkt) {
return nil, io.ErrShortBuffer
}
b, err := aead.Open(dst[:0], _zerononce[:aead.NonceSize()], pkt[saltSize:], nil)
return b, err
}
type packetConn struct {
net.PacketConn
Cipher
sync.Mutex
buf []byte // write lock
}
// NewPacketConn wraps a net.PacketConn with cipher
func NewPacketConn(c net.PacketConn, ciph Cipher) net.PacketConn {
const maxPacketSize = 64 * 1024
return &packetConn{PacketConn: c, Cipher: ciph, buf: make([]byte, maxPacketSize)}
}
// WriteTo encrypts b and write to addr using the embedded PacketConn.
func (c *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) {
c.Lock()
defer c.Unlock()
buf, err := Pack(c.buf, b, c)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(buf, addr)
return len(b), err
}
// ReadFrom reads from the embedded PacketConn and decrypts into b.
func (c *packetConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, addr, err := c.PacketConn.ReadFrom(b)
if err != nil {
return n, 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

@ -1,150 +0,0 @@
// 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

@ -1,143 +0,0 @@
package shadowstream
import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rc4"
"strconv"
"github.com/aead/chacha20"
"github.com/aead/chacha20/chacha"
)
// Cipher generates a pair of stream ciphers for encryption and decryption.
type Cipher interface {
IVSize() int
Encrypter(iv []byte) cipher.Stream
Decrypter(iv []byte) cipher.Stream
}
// KeySizeError is an error about the key size.
type KeySizeError int
func (e KeySizeError) Error() string {
return "key size error: need " + strconv.Itoa(int(e)) + " bytes"
}
// CTR mode
type ctrStream struct{ cipher.Block }
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 {
return nil, err
}
return &ctrStream{blk}, nil
}
// CFB mode
type cfbStream struct{ cipher.Block }
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 {
return nil, err
}
return &cfbStream{blk}, nil
}
// IETF-variant of chacha20
type chacha20ietfkey []byte
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(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) != chacha.KeySize {
return nil, KeySizeError(chacha.KeySize)
}
return chacha20ietfkey(key), nil
}
// xchacha20
type xchacha20key []byte
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(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) != 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

@ -1,62 +0,0 @@
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

@ -1,80 +0,0 @@
package shadowstream
import (
"crypto/rand"
"errors"
"io"
"net"
"sync"
)
// ErrShortPacket means the packet is too short to be a valid encrypted packet.
var ErrShortPacket = errors.New("short packet")
// Pack encrypts plaintext using stream cipher s and a random IV.
// Returns a slice of dst containing random IV and ciphertext.
// Ensure len(dst) >= s.IVSize() + len(plaintext).
func Pack(dst, plaintext []byte, s Cipher) ([]byte, error) {
if len(dst) < s.IVSize()+len(plaintext) {
return nil, io.ErrShortBuffer
}
iv := dst[:s.IVSize()]
_, err := io.ReadFull(rand.Reader, iv)
if err != nil {
return nil, err
}
s.Encrypter(iv).XORKeyStream(dst[len(iv):], plaintext)
return dst[:len(iv)+len(plaintext)], nil
}
// Unpack decrypts pkt using stream cipher s.
// Returns a slice of dst containing decrypted plaintext.
func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) {
if len(pkt) < s.IVSize() {
return nil, ErrShortPacket
}
if len(dst) < len(pkt)-s.IVSize() {
return nil, io.ErrShortBuffer
}
iv := pkt[:s.IVSize()]
s.Decrypter(iv).XORKeyStream(dst, pkt[len(iv):])
return dst[:len(pkt)-len(iv)], nil
}
type packetConn struct {
net.PacketConn
Cipher
buf []byte
sync.Mutex // write lock
}
// NewPacketConn wraps a net.PacketConn with stream cipher encryption/decryption.
func NewPacketConn(c net.PacketConn, ciph Cipher) net.PacketConn {
return &packetConn{PacketConn: c, Cipher: ciph, buf: make([]byte, 64*1024)}
}
func (c *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) {
c.Lock()
defer c.Unlock()
buf, err := Pack(c.buf, b, c.Cipher)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(buf, addr)
return len(b), err
}
func (c *packetConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, addr, err := c.PacketConn.ReadFrom(b)
if err != nil {
return n, 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

@ -1,55 +0,0 @@
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
}

View File

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

View File

@ -1,178 +0,0 @@
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,11 +1,19 @@
package ss
import (
"errors"
"net"
"net/url"
"strings"
"sync"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/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/proxy"
"github.com/nadoo/glider/proxy/ss/cipher"
)
// SS is a base ss struct.
@ -14,7 +22,7 @@ type SS struct {
proxy proxy.Proxy
addr string
cipher.Cipher
core.Cipher
}
func init() {
@ -26,7 +34,7 @@ func init() {
func NewSS(s string, d proxy.Dialer, p proxy.Proxy) (*SS, error) {
u, err := url.Parse(s)
if err != nil {
log.F("[ss] parse err: %s", err)
log.F("parse err: %s", err)
return nil, err
}
@ -34,7 +42,7 @@ func NewSS(s string, d proxy.Dialer, p proxy.Proxy) (*SS, error) {
method := u.User.Username()
pass, _ := u.User.Password()
ciph, err := cipher.PickCipher(method, nil, pass)
ciph, err := core.PickCipher(method, nil, pass)
if err != nil {
log.Fatalf("[ss] PickCipher for '%s', error: %s", method, err)
}
@ -49,18 +57,224 @@ func NewSS(s string, d proxy.Dialer, p proxy.Proxy) (*SS, error) {
return ss, nil
}
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
`)
// NewSSDialer returns a ss proxy dialer.
func NewSSDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
return NewSS(s, d, nil)
}
// 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.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.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)
}
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.proxy.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] %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())
_, _, 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.proxy.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.RelayUDP(c, raddr, pc, 2*time.Minute)
pc.Close()
nm.Delete(raddr.String())
}()
log.F("[ss-udp] %s <-> %s", raddr, c.tgtAddr)
} 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 returns all the ciphers supported.
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
}
// 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
}
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
}

View File

@ -1,199 +0,0 @@
package ssh
import (
"errors"
"net"
"net/url"
"os"
"strconv"
"sync"
"time"
"golang.org/x/crypto/ssh"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/proxy"
)
// SSH is a base ssh struct.
type SSH struct {
dialer proxy.Dialer
proxy proxy.Proxy
addr string
conn net.Conn
client *ssh.Client
config *ssh.ClientConfig
once sync.Once
mutex sync.RWMutex
}
func init() {
proxy.RegisterDialer("ssh", NewSSHDialer)
}
// NewSSH returns a ssh proxy.
func NewSSH(s string, d proxy.Dialer, p proxy.Proxy) (*SSH, error) {
u, err := url.Parse(s)
if err != nil {
log.F("[ssh] parse err: %s", err)
return nil, err
}
user := u.User.Username()
if user == "" {
user = "root"
}
config := &ssh.ClientConfig{
User: user,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
if pass, _ := u.User.Password(); pass != "" {
config.Auth = []ssh.AuthMethod{ssh.Password(pass)}
}
query := u.Query()
if key := query.Get("key"); key != "" {
keyAuth, err := privateKeyAuth(key)
if err != nil {
log.F("[ssh] read key file error: %s", err)
return nil, err
}
config.Auth = append(config.Auth, keyAuth)
}
// timeout of ssh handshake and channel operation
qtimeout := query.Get("timeout")
if qtimeout == "" {
qtimeout = "5" // default timeout
}
timeout, err := strconv.ParseUint(qtimeout, 10, 32)
if err != nil {
log.F("[ssh] parse timeout err: %s", err)
return nil, err
}
config.Timeout = time.Second * time.Duration(timeout)
t := &SSH{
dialer: d,
proxy: p,
addr: u.Host,
config: config,
}
if _, port, _ := net.SplitHostPort(t.addr); port == "" {
t.addr = net.JoinHostPort(t.addr, "22")
}
return t, nil
}
// NewSSHDialer returns a ssh proxy dialer.
func NewSSHDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
return NewSSH(s, d, nil)
}
// Addr returns forwarder's address.
func (s *SSH) 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 *SSH) Dial(network, addr string) (net.Conn, error) {
s.once.Do(func() { go s.keepConn(s.initConn() == nil) })
s.mutex.RLock()
defer s.mutex.RUnlock()
if s.client == nil {
return nil, errors.New("ssh client is nil")
}
return s.dial(network, addr)
}
func (s *SSH) dial(network, addr string) (net.Conn, error) {
s.conn.SetDeadline(time.Now().Add(s.config.Timeout))
c, err := s.client.Dial(network, addr)
s.conn.SetDeadline(time.Time{})
return c, err
}
func (s *SSH) initConn() error {
s.mutex.Lock()
defer s.mutex.Unlock()
log.F("[ssh] connecting to %s", s.addr)
c, err := s.dialer.Dial("tcp", s.addr)
if err != nil {
log.F("[ssh] dial connection to %s error: %s", s.addr, err)
return err
}
c.SetDeadline(time.Now().Add(s.config.Timeout))
conn, ch, req, err := ssh.NewClientConn(c, s.addr, s.config)
if err != nil {
log.F("[ssh] initial connection to %s error: %s", s.addr, err)
c.Close()
return err
}
c.SetDeadline(time.Time{})
s.conn = c
s.client = ssh.NewClient(conn, ch, req)
return nil
}
func (s *SSH) keepConn(connected bool) {
if connected {
s.client.Conn.Wait()
s.conn.Close()
}
sleep := time.Second
for {
if err := s.initConn(); err != nil {
sleep *= 2
if sleep > time.Second*60 {
sleep = time.Second * 60
}
time.Sleep(sleep)
continue
}
sleep = time.Second
s.client.Conn.Wait()
s.conn.Close()
}
}
// DialUDP connects to the given address via the proxy.
func (s *SSH) DialUDP(network, addr string) (pc net.PacketConn, err error) {
return nil, proxy.ErrNotSupported
}
func privateKeyAuth(file string) (ssh.AuthMethod, error) {
buffer, err := os.ReadFile(file)
if err != nil {
return nil, err
}
key, err := ssh.ParsePrivateKey(buffer)
if err != nil {
return nil, err
}
return ssh.PublicKeys(key), nil
}
func init() {
proxy.AddUsage("ssh", `
SSH scheme:
ssh://user[:pass]@host:port[?key=keypath&timeout=SECONDS]
timeout: timeout of ssh handshake and channel operation, default: 5
`)
}

View File

@ -1,342 +0,0 @@
package cipher
import (
"crypto/aes"
"crypto/cipher"
"crypto/des"
"crypto/md5"
"crypto/rand"
"crypto/rc4"
"encoding/binary"
"errors"
"github.com/aead/chacha20"
"github.com/dgryski/go-camellia"
"github.com/dgryski/go-idea"
"github.com/dgryski/go-rc2"
"golang.org/x/crypto/blowfish"
"golang.org/x/crypto/cast5"
"golang.org/x/crypto/salsa20/salsa"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/proxy/ssr/internal/tools"
)
var errEmptyPassword = errors.New("empty key")
type DecOrEnc int
const (
Decrypt DecOrEnc = iota
Encrypt
)
func newCTRStream(block cipher.Block, err error, key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
if err != nil {
return nil, err
}
return cipher.NewCTR(block, iv), nil
}
func newAESCTRStream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
block, err := aes.NewCipher(key)
return newCTRStream(block, err, key, iv, doe)
}
func newOFBStream(block cipher.Block, err error, key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
if err != nil {
return nil, err
}
return cipher.NewCTR(block, iv), nil
}
func newAESOFBStream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
block, err := aes.NewCipher(key)
return newOFBStream(block, err, key, iv, doe)
}
func newCFBStream(block cipher.Block, err error, key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
if err != nil {
return nil, err
}
if doe == Encrypt {
return cipher.NewCFBEncrypter(block, iv), nil
} else {
return cipher.NewCFBDecrypter(block, iv), nil
}
}
func newAESCFBStream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
block, err := aes.NewCipher(key)
return newCFBStream(block, err, key, iv, doe)
}
func newDESStream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
block, err := des.NewCipher(key)
return newCFBStream(block, err, key, iv, doe)
}
func newBlowFishStream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
block, err := blowfish.NewCipher(key)
return newCFBStream(block, err, key, iv, doe)
}
func newCast5Stream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
block, err := cast5.NewCipher(key)
return newCFBStream(block, err, key, iv, doe)
}
func newRC4MD5Stream(key, iv []byte, _ DecOrEnc) (cipher.Stream, error) {
h := md5.New()
h.Write(key)
h.Write(iv)
rc4key := h.Sum(nil)
return rc4.NewCipher(rc4key)
}
func newChaCha20Stream(key, iv []byte, _ DecOrEnc) (cipher.Stream, error) {
return chacha20.NewCipher(iv, key)
}
func newChacha20IETFStream(key, iv []byte, _ DecOrEnc) (cipher.Stream, error) {
return chacha20.NewCipher(iv, key)
}
type salsaStreamCipher struct {
nonce [8]byte
key [32]byte
counter int
}
func (c *salsaStreamCipher) XORKeyStream(dst, src []byte) {
var buf []byte
padLen := c.counter % 64
dataSize := len(src) + padLen
if cap(dst) >= dataSize {
buf = dst[:dataSize]
// nadoo: comment out codes here to use pool buffer
// modify start -->
// } else if leakybuf.GlobalLeakyBufSize >= dataSize {
// buf = leakybuf.GlobalLeakyBuf.Get()
// defer leakybuf.GlobalLeakyBuf.Put(buf)
// buf = buf[:dataSize]
// } else {
// buf = make([]byte, dataSize)
// }
} else {
buf = pool.GetBuffer(dataSize)
defer pool.PutBuffer(buf)
}
// --> modify end
var subNonce [16]byte
copy(subNonce[:], c.nonce[:])
binary.LittleEndian.PutUint64(subNonce[len(c.nonce):], uint64(c.counter/64))
// It's difficult to avoid data copy here. src or dst maybe slice from
// Conn.Read/Write, which can't have padding.
copy(buf[padLen:], src[:])
salsa.XORKeyStream(buf, buf, &subNonce, &c.key)
copy(dst, buf[padLen:])
c.counter += len(src)
}
func newSalsa20Stream(key, iv []byte, _ DecOrEnc) (cipher.Stream, error) {
var c salsaStreamCipher
copy(c.nonce[:], iv[:8])
copy(c.key[:], key[:32])
return &c, nil
}
func newCamelliaStream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
block, err := camellia.New(key)
return newCFBStream(block, err, key, iv, doe)
}
func newIdeaStream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
block, err := idea.NewCipher(key)
return newCFBStream(block, err, key, iv, doe)
}
func newRC2Stream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
block, err := rc2.New(key, 16)
return newCFBStream(block, err, key, iv, doe)
}
func newRC4Stream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
return rc4.NewCipher(key)
}
func newSeedStream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
// TODO: SEED block cipher implementation is required
block, err := rc2.New(key, 16)
return newCFBStream(block, err, key, iv, doe)
}
type NoneStream struct {
cipher.Stream
}
func (*NoneStream) XORKeyStream(dst, src []byte) {
copy(dst, src)
}
func newNoneStream(key, iv []byte, doe DecOrEnc) (cipher.Stream, error) {
return new(NoneStream), nil
}
type cipherInfo struct {
keyLen int
ivLen int
newStream func(key, iv []byte, doe DecOrEnc) (cipher.Stream, error)
}
var streamCipherMethod = map[string]*cipherInfo{
"aes-128-cfb": {16, 16, newAESCFBStream},
"aes-192-cfb": {24, 16, newAESCFBStream},
"aes-256-cfb": {32, 16, newAESCFBStream},
"aes-128-ctr": {16, 16, newAESCTRStream},
"aes-192-ctr": {24, 16, newAESCTRStream},
"aes-256-ctr": {32, 16, newAESCTRStream},
"aes-128-ofb": {16, 16, newAESOFBStream},
"aes-192-ofb": {24, 16, newAESOFBStream},
"aes-256-ofb": {32, 16, newAESOFBStream},
"des-cfb": {8, 8, newDESStream},
"bf-cfb": {16, 8, newBlowFishStream},
"cast5-cfb": {16, 8, newCast5Stream},
"rc4-md5": {16, 16, newRC4MD5Stream},
"rc4-md5-6": {16, 6, newRC4MD5Stream},
"chacha20": {32, 8, newChaCha20Stream},
"chacha20-ietf": {32, 12, newChacha20IETFStream},
"salsa20": {32, 8, newSalsa20Stream},
"camellia-128-cfb": {16, 16, newCamelliaStream},
"camellia-192-cfb": {24, 16, newCamelliaStream},
"camellia-256-cfb": {32, 16, newCamelliaStream},
"idea-cfb": {16, 8, newIdeaStream},
"rc2-cfb": {16, 8, newRC2Stream},
"seed-cfb": {16, 8, newSeedStream},
"rc4": {16, 0, newRC4Stream},
"none": {16, 0, newNoneStream},
}
func CheckCipherMethod(method string) error {
if method == "" {
method = "rc4-md5"
}
_, ok := streamCipherMethod[method]
if !ok {
return errors.New("Unsupported encryption method: " + method)
}
return nil
}
type StreamCipher struct {
enc cipher.Stream
dec cipher.Stream
key []byte
info *cipherInfo
iv []byte
}
// NewStreamCipher creates a cipher that can be used in Dial() etc.
// Use cipher.Copy() to create a new cipher with the same method and password
// to avoid the cost of repeated cipher initialization.
func NewStreamCipher(method, password string) (c *StreamCipher, err error) {
if password == "" {
return nil, errEmptyPassword
}
if method == "" {
method = "rc4-md5"
}
mi, ok := streamCipherMethod[method]
if !ok {
return nil, errors.New("Unsupported encryption method: " + method)
}
key := tools.EVPBytesToKey(password, mi.keyLen)
c = &StreamCipher{key: key, info: mi}
return c, nil
}
func (c *StreamCipher) EncryptInited() bool {
return c.enc != nil
}
func (c *StreamCipher) DecryptInited() bool {
return c.dec != nil
}
// Initializes the block cipher with CFB mode, returns IV.
func (c *StreamCipher) InitEncrypt() (iv []byte, err error) {
if c.iv == nil {
iv = make([]byte, c.info.ivLen)
rand.Read(iv)
c.iv = iv
} else {
iv = c.iv
}
c.enc, err = c.info.newStream(c.key, iv, Encrypt)
return
}
func (c *StreamCipher) InitDecrypt(iv []byte) (err error) {
c.dec, err = c.info.newStream(c.key, iv, Decrypt)
return
}
func (c *StreamCipher) Encrypt(dst, src []byte) {
c.enc.XORKeyStream(dst, src)
}
func (c *StreamCipher) Decrypt(dst, src []byte) {
c.dec.XORKeyStream(dst, src)
}
// Copy creates a new cipher at it's initial state.
func (c *StreamCipher) Copy() *StreamCipher {
// This optimization maybe not necessary. But without this function, we
// need to maintain a table cache for newTableCipher and use lock to
// protect concurrent access to that cache.
// AES and DES ciphers does not return specific types, so it's difficult
// to create copy. But their initialization time is less than 4000ns on my
// 2.26 GHz Intel Core 2 Duo processor. So no need to worry.
// Currently, blow-fish and cast5 initialization cost is an order of
// magnitude slower than other ciphers. (I'm not sure whether this is
// because the current implementation is not highly optimized, or this is
// the nature of the algorithm.)
nc := *c
nc.enc = nil
nc.dec = nil
return &nc
}
func (c *StreamCipher) Key() []byte {
return c.key
}
func (c *StreamCipher) IV() []byte {
return c.iv
}
func (c *StreamCipher) SetIV(iv []byte) {
c.iv = iv
}
func (c *StreamCipher) SetKey(key []byte) {
c.key = key
}
func (c *StreamCipher) InfoIVLen() int {
return c.info.ivLen
}
func (c *StreamCipher) InfoKeyLen() int {
return c.info.keyLen
}

View File

@ -1,228 +0,0 @@
// source code from https://github.com/v2rayA/shadowsocksR
// Just copy here to use glider's builtin buffer pool.
// As this protocol hasn't been maintained since 2017, it doesn't deserve our research to rewrite it.
package internal
import (
"bytes"
"errors"
"fmt"
"net"
"github.com/nadoo/glider/pkg/pool"
"github.com/nadoo/glider/proxy"
"github.com/nadoo/glider/proxy/ssr/internal/cipher"
"github.com/nadoo/glider/proxy/ssr/internal/obfs"
"github.com/nadoo/glider/proxy/ssr/internal/protocol"
)
var bufSize = proxy.TCPBufSize
// SSTCPConn the struct that override the net.Conn methods
type SSTCPConn struct {
net.Conn
*cipher.StreamCipher
IObfs obfs.IObfs
IProtocol protocol.IProtocol
readBuf []byte
underPostdecryptBuf *bytes.Buffer
readIndex uint64
decryptedBuf *bytes.Buffer
writeBuf []byte
lastReadError error
}
func NewSSTCPConn(c net.Conn, cipher *cipher.StreamCipher) *SSTCPConn {
return &SSTCPConn{
Conn: c,
StreamCipher: cipher,
readBuf: pool.GetBuffer(bufSize),
decryptedBuf: pool.GetBytesBuffer(),
underPostdecryptBuf: pool.GetBytesBuffer(),
writeBuf: pool.GetBuffer(bufSize),
}
}
func (c *SSTCPConn) Close() error {
pool.PutBuffer(c.readBuf)
pool.PutBytesBuffer(c.decryptedBuf)
pool.PutBytesBuffer(c.underPostdecryptBuf)
pool.PutBuffer(c.writeBuf)
return c.Conn.Close()
}
func (c *SSTCPConn) GetIv() (iv []byte) {
iv = make([]byte, len(c.IV()))
copy(iv, c.IV())
return
}
func (c *SSTCPConn) GetKey() (key []byte) {
key = make([]byte, len(c.Key()))
copy(key, c.Key())
return
}
func (c *SSTCPConn) initEncryptor(b []byte) (iv []byte, err error) {
if !c.EncryptInited() {
iv, err = c.InitEncrypt()
if err != nil {
return nil, err
}
overhead := c.IObfs.GetOverhead() + c.IProtocol.GetOverhead()
// should initialize obfs/protocol now, because iv is ready now
obfsServerInfo := c.IObfs.GetServerInfo()
obfsServerInfo.SetHeadLen(b, 30)
obfsServerInfo.IV, obfsServerInfo.IVLen = c.IV(), c.InfoIVLen()
obfsServerInfo.Key, obfsServerInfo.KeyLen = c.Key(), c.InfoKeyLen()
obfsServerInfo.Overhead = overhead
c.IObfs.SetServerInfo(obfsServerInfo)
protocolServerInfo := c.IProtocol.GetServerInfo()
protocolServerInfo.SetHeadLen(b, 30)
protocolServerInfo.IV, protocolServerInfo.IVLen = c.IV(), c.InfoIVLen()
protocolServerInfo.Key, protocolServerInfo.KeyLen = c.Key(), c.InfoKeyLen()
protocolServerInfo.Overhead = overhead
c.IProtocol.SetServerInfo(protocolServerInfo)
}
return
}
func (c *SSTCPConn) Read(b []byte) (n int, err error) {
for {
n, err = c.doRead(b)
if b == nil || n != 0 || err != nil {
return n, err
}
}
}
func (c *SSTCPConn) doRead(b []byte) (n int, err error) {
if c.decryptedBuf.Len() > 0 {
return c.decryptedBuf.Read(b)
}
n, err = c.Conn.Read(c.readBuf)
if n == 0 || err != nil {
return n, err
}
decodedData, needSendBack, err := c.IObfs.Decode(c.readBuf[:n])
if err != nil {
return 0, err
}
//do send back
if needSendBack {
c.Write(nil)
return 0, nil
}
decodedDataLen := len(decodedData)
if decodedDataLen == 0 {
return 0, nil
}
if !c.DecryptInited() {
if len(decodedData) < c.InfoIVLen() {
return 0, errors.New(fmt.Sprintf("invalid ivLen:%v, actual length:%v", c.InfoIVLen(), len(decodedData)))
}
iv := decodedData[0:c.InfoIVLen()]
if err = c.InitDecrypt(iv); err != nil {
return 0, err
}
if len(c.IV()) == 0 {
c.SetIV(iv)
}
decodedDataLen -= c.InfoIVLen()
if decodedDataLen <= 0 {
return 0, nil
}
decodedData = decodedData[c.InfoIVLen():]
}
buf1 := pool.GetBuffer(decodedDataLen)
defer pool.PutBuffer(buf1)
c.Decrypt(buf1, decodedData)
c.underPostdecryptBuf.Write(buf1)
buf := c.underPostdecryptBuf.Bytes()
postDecryptedData, length, err := c.IProtocol.PostDecrypt(buf)
if err != nil {
c.underPostdecryptBuf.Reset()
return 0, err
}
if length == 0 {
// not enough to postDecrypt
return 0, nil
} else {
c.underPostdecryptBuf.Next(length)
}
postDecryptedLength := len(postDecryptedData)
blength := len(b)
if blength >= postDecryptedLength {
copy(b, postDecryptedData)
return postDecryptedLength, nil
}
copy(b, postDecryptedData[:blength])
c.decryptedBuf.Write(postDecryptedData[blength:])
return blength, nil
}
func (c *SSTCPConn) preWrite(b []byte) (outData []byte, err error) {
if b == nil {
b = make([]byte, 0)
}
var iv []byte
if iv, err = c.initEncryptor(b); err != nil {
return
}
var preEncryptedData []byte
preEncryptedData, err = c.IProtocol.PreEncrypt(b)
if err != nil {
return
}
preEncryptedDataLen := len(preEncryptedData)
//! \attention here the expected output buffer length MUST be accurate, it is preEncryptedDataLen now!
cipherData := c.writeBuf
dataSize := preEncryptedDataLen + len(iv)
if dataSize > len(cipherData) {
cipherData = make([]byte, dataSize)
} else {
cipherData = cipherData[:dataSize]
}
if iv != nil {
// Put initialization vector in buffer before be encoded
copy(cipherData, iv)
}
c.Encrypt(cipherData[len(iv):], preEncryptedData)
return c.IObfs.Encode(cipherData)
}
func (c *SSTCPConn) Write(b []byte) (n int, err error) {
outData, err := c.preWrite(b)
if err != nil {
return 0, err
}
n, err = c.Conn.Write(outData)
if err != nil {
return 0, err
}
return len(b), nil
}

View File

@ -1,36 +0,0 @@
package obfs
import (
"strings"
"github.com/nadoo/glider/proxy/ssr/internal/ssr"
)
type creator func() IObfs
var (
creatorMap = make(map[string]creator)
)
type IObfs interface {
SetServerInfo(s *ssr.ServerInfo)
GetServerInfo() (s *ssr.ServerInfo)
Encode(data []byte) (encodedData []byte, err error)
Decode(data []byte) (decodedData []byte, needSendBack bool, err error)
SetData(data any)
GetData() any
GetOverhead() int
}
func register(name string, c creator) {
creatorMap[name] = c
}
// NewObfs create an obfs object by name and return as an IObfs interface
func NewObfs(name string) IObfs {
c, ok := creatorMap[strings.ToLower(name)]
if ok {
return c()
}
return nil
}

View File

@ -1,20 +0,0 @@
package obfs
import (
"math/rand/v2"
)
func init() {
register("http_post", newHttpPost)
}
// newHttpPost create a http_post object
func newHttpPost() IObfs {
// newHttpSimple create a http_simple object
t := &httpSimplePost{
userAgentIndex: rand.IntN(len(requestUserAgent)),
methodGet: false,
}
return t
}

View File

@ -1,185 +0,0 @@
package obfs
import (
"bytes"
"encoding/hex"
"fmt"
"math/rand/v2"
"strings"
"github.com/nadoo/glider/proxy/ssr/internal/ssr"
)
var (
requestPath = []string{
"", "",
"login.php?redir=", "",
"register.php?code=", "",
"?keyword=", "",
"search?src=typd&q=", "&lang=en",
"s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&bar=&wd=", "&rn=",
"post.php?id=", "&goto=view.php",
}
requestUserAgent = []string{
"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0",
"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/44.0",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/27.0.1453.93 Chrome/27.0.1453.93 Safari/537.36",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0",
"Mozilla/5.0 (compatible; WOW64; MSIE 10.0; Windows NT 6.2)",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0; .NET4.0E; .NET4.0C)",
"Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
"Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/BuildID) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36",
"Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
"Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
}
)
// HttpSimple http_simple obfs encapsulate
type httpSimplePost struct {
ssr.ServerInfo
rawTransSent bool
rawTransReceived bool
userAgentIndex int
methodGet bool // true for get, false for post
}
func init() {
register("http_simple", newHttpSimple)
}
// newHttpSimple create a http_simple object
func newHttpSimple() IObfs {
t := &httpSimplePost{
rawTransSent: false,
rawTransReceived: false,
userAgentIndex: rand.IntN(len(requestUserAgent)),
methodGet: true,
}
return t
}
func (t *httpSimplePost) SetServerInfo(s *ssr.ServerInfo) {
t.ServerInfo = *s
}
func (t *httpSimplePost) GetServerInfo() (s *ssr.ServerInfo) {
return &t.ServerInfo
}
func (t *httpSimplePost) SetData(data any) {
}
func (t *httpSimplePost) GetData() any {
return nil
}
func (t *httpSimplePost) boundary() (ret string) {
set := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for range 32 {
ret = fmt.Sprintf("%s%c", ret, set[rand.IntN(len(set))])
}
return
}
func (t *httpSimplePost) data2URLEncode(data []byte) (ret string) {
for i := range data {
ret = fmt.Sprintf("%s%%%s", ret, hex.EncodeToString([]byte{data[i]}))
}
return
}
func (t *httpSimplePost) Encode(data []byte) (encodedData []byte, err error) {
if t.rawTransSent {
return data, nil
}
dataLength := len(data)
var headData []byte
if headSize := t.IVLen + t.HeadLen; dataLength-headSize > 64 {
headData = make([]byte, headSize+rand.IntN(64))
} else {
headData = make([]byte, dataLength)
}
copy(headData, data[0:len(headData)])
requestPathIndex := rand.IntN(len(requestPath)/2) * 2
host := t.Host
var customHead string
if len(t.Param) > 0 {
customHeads := strings.Split(t.Param, "#")
if len(customHeads) > 2 {
customHeads = customHeads[0:2]
}
param := t.Param
if len(customHeads) > 1 {
customHead = customHeads[1]
param = customHeads[0]
}
hosts := strings.Split(param, ",")
if len(hosts) > 0 {
host = strings.TrimSpace(hosts[rand.IntN(len(hosts))])
}
}
method := "GET /"
if !t.methodGet {
method = "POST /"
}
httpBuf := fmt.Sprintf("%s%s%s%s HTTP/1.1\r\nHost: %s:%d\r\n",
method,
requestPath[requestPathIndex],
t.data2URLEncode(headData),
requestPath[requestPathIndex+1],
host,
t.Port)
if len(customHead) > 0 {
httpBuf = httpBuf + strings.Replace(customHead, "\\n", "\r\n", -1) + "\r\n\r\n"
} else {
var contentType string
if !t.methodGet {
contentType = "Content-Type: multipart/form-data; boundary=" + t.boundary() + "\r\n"
}
httpBuf = httpBuf +
"User-Agent: " + requestUserAgent[t.userAgentIndex] + "\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
"Accept-Language: en-US,en;q=0.8\r\n" +
"Accept-Encoding: gzip, deflate\r\n" +
contentType +
"DNT: 1\r\n" +
"Connection: keep-alive\r\n" +
"\r\n"
}
if len(headData) < dataLength {
encodedData = make([]byte, len(httpBuf)+(dataLength-len(headData)))
copy(encodedData, []byte(httpBuf))
copy(encodedData[len(httpBuf):], data[len(headData):])
} else {
encodedData = []byte(httpBuf)
}
t.rawTransSent = true
return
}
func (t *httpSimplePost) Decode(data []byte) (decodedData []byte, needSendBack bool, err error) {
if t.rawTransReceived {
return data, false, nil
}
pos := bytes.Index(data, []byte("\r\n\r\n"))
if pos > 0 {
decodedData = make([]byte, len(data)-pos-4)
copy(decodedData, data[pos+4:])
t.rawTransReceived = true
}
return decodedData, false, nil
}
func (t *httpSimplePost) GetOverhead() int {
return 0
}

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