mirror of
https://github.com/nadoo/glider.git
synced 2025-04-29 23:49:58 +08:00
Compare commits
No commits in common. "v0.4.3" and "main" have entirely different histories.
38
.Dockerfile
Normal file
38
.Dockerfile
Normal file
@ -0,0 +1,38 @@
|
||||
# Build Stage
|
||||
FROM --platform=$BUILDPLATFORM alpine AS build-env
|
||||
|
||||
COPY ./dist /dist
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
RUN case $TARGETPLATFORM in \
|
||||
'linux/386') \
|
||||
export FOLDER='default_linux_386_sse2'; \
|
||||
;; \
|
||||
'linux/amd64') \
|
||||
export FOLDER='default_linux_amd64_v1'; \
|
||||
;; \
|
||||
'linux/arm/v6') \
|
||||
export FOLDER='default_linux_arm_6'; \
|
||||
;; \
|
||||
'linux/arm/v7') \
|
||||
export FOLDER='default_linux_arm_7'; \
|
||||
;; \
|
||||
'linux/arm64') \
|
||||
export FOLDER='default_linux_arm64_v8.0'; \
|
||||
;; \
|
||||
'linux/riscv64') \
|
||||
export FOLDER='default_linux_riscv64_rva20u64'; \
|
||||
;; \
|
||||
*) echo >&2 "error: unsupported architecture '$TARGETPLATFORM'"; exit 1 ;; \
|
||||
esac \
|
||||
&& mv /dist/$FOLDER /app
|
||||
|
||||
|
||||
# Final Stage
|
||||
FROM scratch
|
||||
COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=build-env /app /app
|
||||
WORKDIR /app
|
||||
USER 1000
|
||||
ENTRYPOINT ["./glider"]
|
103
.github/workflows/build.yml
vendored
Normal file
103
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
name: Build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "dev"
|
||||
tags:
|
||||
- "*"
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
APP_NAME: glider
|
||||
DOCKERHUB_REPO: nadoo/glider
|
||||
GHCR_REPO: ghcr.io/nadoo/glider
|
||||
PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/riscv64
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set Vars
|
||||
run: |
|
||||
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
check-latest: true
|
||||
go-version-file: "go.mod"
|
||||
cache: true
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
if: "!startsWith(github.ref, 'refs/tags/')"
|
||||
with:
|
||||
args: build --snapshot --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
if: "!startsWith(github.ref, 'refs/tags/')"
|
||||
with:
|
||||
name: ${{ env.APP_NAME }}-dev-${{ env.SHA_SHORT }}
|
||||
path: |
|
||||
./dist/default_linux_amd64_v1/${{ env.APP_NAME }}
|
||||
./dist/default_linux_arm64/${{ env.APP_NAME }}
|
||||
./dist/default_darwin_arm64/${{ env.APP_NAME }}
|
||||
./dist/default_windows_amd64_v1/${{ env.APP_NAME }}.exe
|
||||
|
||||
- name: Release
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker - Set up Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker - Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker - Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker - Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_REPO }}
|
||||
${{ env.GHCR_REPO }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Docker - Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: .Dockerfile
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
17
.github/workflows/stale.yml
vendored
Normal file
17
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
name: 'Close stale issues'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
days-before-stale: 90
|
||||
days-before-close: 5
|
||||
exempt-issue-labels: "bug,enhancement"
|
||||
exempt-pr-labels: "bug,enhancement"
|
||||
|
14
.gitignore
vendored
14
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
@ -14,6 +15,19 @@
|
||||
.glide/
|
||||
|
||||
# custom
|
||||
.idea
|
||||
.vscode
|
||||
.zed
|
||||
.DS_Store
|
||||
|
||||
# dev test only
|
||||
/dev/
|
||||
dev*.go
|
||||
|
||||
*_test.go
|
||||
|
||||
dist
|
||||
|
||||
*.zip
|
||||
/*.conf
|
||||
/*.rule
|
||||
|
96
.goreleaser.yml
Normal file
96
.goreleaser.yml
Normal file
@ -0,0 +1,96 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- id: default
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- windows
|
||||
- linux
|
||||
- darwin
|
||||
- freebsd
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- mips
|
||||
- mipsle
|
||||
- mips64
|
||||
- mips64le
|
||||
- riscv64
|
||||
goamd64:
|
||||
- v1
|
||||
- v3
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
gomips:
|
||||
- hardfloat
|
||||
- softfloat
|
||||
|
||||
archives:
|
||||
- id: default
|
||||
builds:
|
||||
- default
|
||||
wrap_in_directory: true
|
||||
formats: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: zip
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
- config/**/*
|
||||
- systemd/*
|
||||
|
||||
snapshot:
|
||||
version_template: '{{ incpatch .Version }}-dev-{{.ShortCommit}}'
|
||||
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"
|
||||
|
||||
release:
|
||||
prerelease: true
|
||||
draft: true
|
||||
|
||||
nfpms:
|
||||
- id: glider
|
||||
package_name: glider
|
||||
vendor: nadoo
|
||||
homepage: https://github.com/nadoo/glider
|
||||
maintainer: nadoo
|
||||
description: Glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset management features(like dnsmasq).
|
||||
license: GPL-3.0 License
|
||||
formats:
|
||||
# - apk
|
||||
- deb
|
||||
# - rpm
|
||||
dependencies:
|
||||
- libsystemd0
|
||||
bindir: /usr/bin
|
||||
release: 1
|
||||
epoch: 1
|
||||
version_metadata: git
|
||||
section: default
|
||||
priority: extra
|
||||
contents:
|
||||
- src: systemd/glider@.service
|
||||
dst: /etc/systemd/system/glider@.service
|
||||
|
||||
- src: config/glider.conf.example
|
||||
dst: /etc/glider/glider.conf.example
|
||||
|
||||
scripts:
|
||||
postinstall: "systemd/postinstall.sh"
|
||||
preremove: "systemd/preremove.sh"
|
||||
postremove: "systemd/postremove.sh"
|
||||
|
||||
deb:
|
||||
triggers:
|
||||
interest_noawait:
|
||||
- /lib/systemd/systemd
|
@ -1,6 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.9
|
||||
- tip
|
||||
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
# Build Stage
|
||||
FROM golang:1.24-alpine AS build-env
|
||||
ADD . /src
|
||||
RUN apk --no-cache add git \
|
||||
&& cd /src && go build -v -ldflags "-s -w"
|
||||
|
||||
# Final Stage
|
||||
FROM alpine
|
||||
COPY --from=build-env /src/glider /app/
|
||||
WORKDIR /app
|
||||
RUN apk -U upgrade --no-cache \
|
||||
&& apk --no-cache add ca-certificates
|
||||
USER 1000
|
||||
ENTRYPOINT ["./glider"]
|
602
README.md
602
README.md
@ -1,173 +1,431 @@
|
||||
# [glider](https://github.com/nadoo/glider)
|
||||
|
||||
[](https://travis-ci.org/nadoo/glider)
|
||||
[](https://go.dev/dl/)
|
||||
[](https://goreportcard.com/report/github.com/nadoo/glider)
|
||||
[](https://github.com/nadoo/glider/releases)
|
||||
[](https://github.com/nadoo/glider/releases)
|
||||
[](https://github.com/nadoo/glider/releases)
|
||||
[](https://github.com/nadoo/glider/actions)
|
||||
[](https://hub.docker.com/r/nadoo/glider)
|
||||
|
||||
glider is a forward proxy with multiple protocols support, and also a dns forwarding server with ipset management features(like dnsmasq).
|
||||
glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset management features(like dnsmasq).
|
||||
|
||||
we can set up local listeners as proxy servers, and forward requests to internet via forwarders.
|
||||
```
|
||||
|
||||
```bash
|
||||
|Forwarder ----------------->|
|
||||
Listener --> | | Internet
|
||||
|Forwarder --> Forwarder->...|
|
||||
```
|
||||
|
||||
## Features
|
||||
Listen(local proxy server):
|
||||
- Socks5 proxy
|
||||
- Http proxy
|
||||
- SS proxy
|
||||
- Linux transparent proxy(iptables redirect)
|
||||
- TCP tunnel
|
||||
- DNS Tunnel(udp2tcp)
|
||||
- Act as both proxy client and proxy server(protocol converter)
|
||||
- Flexible proxy & protocol chains
|
||||
- Load balancing with the following scheduling algorithm:
|
||||
- rr: round robin
|
||||
- ha: high availability
|
||||
- lha: latency based high availability
|
||||
- dh: destination hashing
|
||||
- Rule & priority based forwarder choosing: [Config Examples](config/examples)
|
||||
- DNS forwarding server:
|
||||
- dns over proxy
|
||||
- force upstream querying by tcp
|
||||
- association rules between dns and forwarder choosing
|
||||
- association rules between dns and ipset
|
||||
- dns cache support
|
||||
- custom dns record
|
||||
- IPSet management (linux kernel version >= 2.6.32):
|
||||
- add ip/cidrs from rule files on startup
|
||||
- add resolved ips for domains from rule files by dns forwarding server
|
||||
- Serve http and socks5 on the same port
|
||||
- Periodical availability checking for forwarders
|
||||
- Send requests from specific local ip/interface
|
||||
- Services:
|
||||
- dhcpd: a simple dhcp server that can run in failover mode
|
||||
|
||||
Forward(upstream proxy server):
|
||||
- Socks5 proxy
|
||||
- Http proxy
|
||||
- SS proxy
|
||||
## Protocols
|
||||
|
||||
DNS Forwarding Server(udp2tcp):
|
||||
- Listen on UDP and forward dns requests to remote dns server in TCP via forwarders
|
||||
- Specify different upstream dns server based on destinations(in rule file)
|
||||
- Tunnel mode: forward to a fixed upstream dns server
|
||||
- Add resolved IPs to proxy rules
|
||||
- Add resolved IPs to ipset
|
||||
<details>
|
||||
<summary>click to see details</summary>
|
||||
|
||||
IPSet Management:
|
||||
- Add ip/cidrs from rule files on startup
|
||||
- Add resolved ips for domains from rule files by dns forwarding server
|
||||
|Protocol | Listen/TCP | Listen/UDP | Forward/TCP | Forward/UDP | Description
|
||||
|:-: |:-:|:-:|:-:|:-:|:-
|
||||
|Mixed |√|√| | |http+socks5 server
|
||||
|HTTP |√| |√| |client & server
|
||||
|SOCKS5 |√|√|√|√|client & server
|
||||
|SS |√|√|√|√|client & server
|
||||
|Trojan |√|√|√|√|client & server
|
||||
|Trojanc |√|√|√|√|trojan cleartext(without tls)
|
||||
|VLESS |√|√|√|√|client & server
|
||||
|VMess | | |√|√|client only
|
||||
|SSR | | |√| |client only
|
||||
|SSH | | |√| |client only
|
||||
|SOCKS4 | | |√| |client only
|
||||
|SOCKS4A | | |√| |client only
|
||||
|TCP |√| |√| |tcp tunnel client & server
|
||||
|UDP | |√| |√|udp tunnel client & server
|
||||
|TLS |√| |√| |transport client & server
|
||||
|KCP | |√|√| |transport client & server
|
||||
|Unix |√|√|√|√|transport client & server
|
||||
|VSOCK |√| |√| |transport client & server
|
||||
|Smux |√| |√| |transport client & server
|
||||
|Websocket(WS) |√| |√| |transport client & server
|
||||
|WS Secure |√| |√| |websocket secure (wss)
|
||||
|Proxy Protocol |√| | | |version 1 server only
|
||||
|Simple-Obfs | | |√| |transport client only
|
||||
|Redir |√| | | |linux redirect proxy
|
||||
|Redir6 |√| | | |linux redirect proxy(ipv6)
|
||||
|TProxy | |√| | |linux tproxy(udp only)
|
||||
|Reject | | |√|√|reject all requests
|
||||
|
||||
General:
|
||||
- Http and socks5 on the same port
|
||||
- Forward chain
|
||||
- HA or RR strategy for multiple forwarders
|
||||
- Periodical proxy checking
|
||||
- Rule proxy based on destinations: [Config Examples](config/examples)
|
||||
|
||||
TODO:
|
||||
- [x] UDP over TCP Tunnel (client <--udp--> glider/uottun <--tcp--> ss <--udp--> target)
|
||||
- [ ] Transparent UDP proxy (linux tproxy)
|
||||
- [ ] TUN/TAP device support
|
||||
- [ ] Code refactoring: support proxy registering so it can be pluggable
|
||||
- [ ] Conditional compilation so we can abandon needless proxy type and get a smaller binary size
|
||||
- [ ] IPv6 support
|
||||
- [ ] SSH tunnel support
|
||||
</details>
|
||||
|
||||
## Install
|
||||
Binary:
|
||||
- [https://github.com/nadoo/glider/releases](https://github.com/nadoo/glider/releases)
|
||||
|
||||
Go Get (requires **Go 1.9+** ):
|
||||
```bash
|
||||
go get -u github.com/nadoo/glider
|
||||
```
|
||||
|
||||
ArchLinux:
|
||||
```bash
|
||||
sudo pacman -S glider
|
||||
```
|
||||
|
||||
## Run
|
||||
command line:
|
||||
```bash
|
||||
glider -listen :8443 -verbose
|
||||
```
|
||||
|
||||
config file:
|
||||
```bash
|
||||
glider -config CONFIGPATH
|
||||
```
|
||||
|
||||
command line with config file:
|
||||
```bash
|
||||
glider -config CONFIGPATH -listen :8080 -verbose
|
||||
```
|
||||
- Binary: [https://github.com/nadoo/glider/releases](https://github.com/nadoo/glider/releases)
|
||||
- Docker: `docker pull nadoo/glider`
|
||||
- Manjaro: `pamac install glider`
|
||||
- ArchLinux: `sudo pacman -S glider`
|
||||
- Homebrew: `brew install glider`
|
||||
- MacPorts: `sudo port install glider`
|
||||
- Source: `go install github.com/nadoo/glider@latest`
|
||||
|
||||
## Usage
|
||||
|
||||
#### Run
|
||||
|
||||
```bash
|
||||
glider v0.3.1 usage:
|
||||
-checkduration int
|
||||
proxy check duration(seconds) (default 30)
|
||||
-checkwebsite string
|
||||
proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80 (default "www.apple.com")
|
||||
glider -verbose -listen :8443
|
||||
# docker run --rm -it nadoo/glider -verbose -listen :8443
|
||||
```
|
||||
|
||||
#### Help
|
||||
|
||||
<details>
|
||||
<summary><code>glider -help</code></summary>
|
||||
|
||||
```bash
|
||||
Usage: glider [-listen URL]... [-forward URL]... [OPTION]...
|
||||
|
||||
e.g. glider -config /etc/glider/glider.conf
|
||||
glider -listen :8443 -forward socks5://serverA:1080 -forward socks5://serverB:1080 -verbose
|
||||
|
||||
OPTION:
|
||||
-check string
|
||||
check=tcp[://HOST:PORT]: tcp port connect check
|
||||
check=http://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
|
||||
check=https://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
|
||||
check=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, env vars: FORWARDER_ADDR,FORWARDER_URL
|
||||
check=disable: disable health check (default "http://www.msftconnecttest.com/connecttest.txt#expect=200")
|
||||
-checkdisabledonly
|
||||
check disabled fowarders only
|
||||
-checkinterval int
|
||||
fowarder check interval(seconds) (default 30)
|
||||
-checklatencysamples int
|
||||
use the average latency of the latest N checks (default 10)
|
||||
-checktimeout int
|
||||
fowarder check timeout(seconds) (default 10)
|
||||
-checktolerance int
|
||||
fowarder check tolerance(ms), switch only when new_latency < old_latency - tolerance, only used in lha mode
|
||||
-config string
|
||||
config file path
|
||||
-dialtimeout int
|
||||
dial timeout(seconds) (default 3)
|
||||
-dns string
|
||||
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, format: SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT[,SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT]
|
||||
forward url, see the URL section below
|
||||
-include value
|
||||
include file
|
||||
-interface string
|
||||
source ip or source interface
|
||||
-listen value
|
||||
listen url, format: SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT
|
||||
listen url, see the URL section below
|
||||
-logflags int
|
||||
do not change it if you do not know what it is, ref: https://pkg.go.dev/log#pkg-constants (default 19)
|
||||
-maxfailures int
|
||||
max failures to change forwarder status to disabled (default 3)
|
||||
-relaytimeout int
|
||||
relay timeout(seconds)
|
||||
-rulefile value
|
||||
rule file path
|
||||
-rules-dir string
|
||||
rule file folder
|
||||
-scheme string
|
||||
show help message of proxy scheme, use 'all' to see all schemes
|
||||
-service value
|
||||
run specified services, format: SERVICE_NAME[,SERVICE_CONFIG]
|
||||
-strategy string
|
||||
forward strategy, default: rr (default "rr")
|
||||
rr: Round Robin mode
|
||||
ha: High Availability mode
|
||||
lha: Latency based High Availability mode
|
||||
dh: Destination Hashing mode (default "rr")
|
||||
-tcpbufsize int
|
||||
tcp buffer size in Bytes (default 32768)
|
||||
-udpbufsize int
|
||||
udp buffer size in Bytes (default 2048)
|
||||
-verbose
|
||||
verbose mode
|
||||
|
||||
Available Schemas:
|
||||
mixed: serve as a http/socks5 proxy on the same port. (default)
|
||||
ss: ss proxy
|
||||
socks5: socks5 proxy
|
||||
http: http proxy
|
||||
redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules)
|
||||
tcptun: a simple tcp tunnel
|
||||
dnstun: listen on udp port and forward all dns requests to remote dns server via forwarders(tcp)
|
||||
URL:
|
||||
proxy: SCHEME://[USER:PASS@][HOST]:PORT
|
||||
chain: proxy,proxy[,proxy]...
|
||||
|
||||
Available schemas for different modes:
|
||||
listen: mixed ss socks5 http redir tcptun dnstun
|
||||
forward: ss socks5 http
|
||||
e.g. -listen socks5://:1080
|
||||
-listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// (protocol chain)
|
||||
|
||||
e.g. -forward socks5://server:1080
|
||||
-forward tls://server.com:443,http:// (protocol chain)
|
||||
-forward socks5://serverA:1080,socks5://serverB:1080 (proxy chain)
|
||||
|
||||
SCHEME:
|
||||
listen : http kcp mixed pxyproto redir redir6 smux sni socks5 ss tcp tls tproxy trojan trojanc udp unix vless vsock ws wss
|
||||
forward: direct http kcp reject simple-obfs smux socks4 socks4a socks5 ss ssh ssr tcp tls trojan trojanc udp unix vless vmess vsock ws wss
|
||||
|
||||
Note: use 'glider -scheme all' or 'glider -scheme SCHEME' to see help info for the scheme.
|
||||
|
||||
--
|
||||
Forwarder Options: FORWARD_URL#OPTIONS
|
||||
priority : the priority of that forwarder, the larger the higher, default: 0
|
||||
interface: the local interface or ip address used to connect remote server.
|
||||
|
||||
e.g. -forward socks5://server:1080#priority=100
|
||||
-forward socks5://server:1080#interface=eth0
|
||||
-forward socks5://server:1080#priority=100&interface=192.168.1.99
|
||||
|
||||
Services:
|
||||
dhcpd: service=dhcpd,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
|
||||
service=dhcpd-failover,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
|
||||
e.g. service=dhcpd,eth1,192.168.1.100,192.168.1.199,720
|
||||
|
||||
--
|
||||
Help:
|
||||
glider -help
|
||||
glider -scheme all
|
||||
glider -example
|
||||
|
||||
see README.md and glider.conf.example for more details.
|
||||
--
|
||||
glider 0.16.4, https://github.com/nadoo/glider (glider.proxy@gmail.com)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Schemes
|
||||
|
||||
<details>
|
||||
<summary><code>glider -scheme all</code></summary>
|
||||
|
||||
```bash
|
||||
Direct scheme:
|
||||
direct://
|
||||
|
||||
Only needed when you want to specify the outgoing interface:
|
||||
glider -verbose -listen :8443 -forward direct://#interface=eth0
|
||||
|
||||
Or load balance multiple interfaces directly:
|
||||
glider -verbose -listen :8443 -forward direct://#interface=eth0 -forward direct://#interface=eth1 -strategy rr
|
||||
|
||||
Or you can use the high availability mode:
|
||||
glider -verbose -listen :8443 -forward direct://#interface=eth0&priority=100 -forward direct://#interface=eth1&priority=200 -strategy ha
|
||||
|
||||
--
|
||||
Http scheme:
|
||||
http://[user:pass@]host:port
|
||||
|
||||
--
|
||||
KCP scheme:
|
||||
kcp://CRYPT:KEY@host:port[?dataShards=NUM&parityShards=NUM&mode=MODE]
|
||||
|
||||
Available crypt types for KCP:
|
||||
none, sm4, tea, xor, aes, aes-128, aes-192, blowfish, twofish, cast5, 3des, xtea, salsa20
|
||||
|
||||
Available modes for KCP:
|
||||
fast, fast2, fast3, normal, default: fast
|
||||
|
||||
--
|
||||
Simple-Obfs scheme:
|
||||
simple-obfs://host:port[?type=TYPE&host=HOST&uri=URI&ua=UA]
|
||||
|
||||
Available types for simple-obfs:
|
||||
http, tls
|
||||
|
||||
--
|
||||
Reject scheme:
|
||||
reject://
|
||||
|
||||
--
|
||||
Smux scheme:
|
||||
smux://host:port
|
||||
|
||||
--
|
||||
Socks4 scheme:
|
||||
socks4://host:port
|
||||
|
||||
--
|
||||
Socks5 scheme:
|
||||
socks5://[user:pass@]host:port
|
||||
|
||||
--
|
||||
SS scheme:
|
||||
ss://method:pass@host:port
|
||||
|
||||
Available methods for ss:
|
||||
AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20
|
||||
NOTE: chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305
|
||||
AEAD Ciphers:
|
||||
AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AEAD_XCHACHA20_POLY1305
|
||||
Stream Ciphers:
|
||||
AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20 CHACHA20 RC4-MD5
|
||||
Alias:
|
||||
chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305, xchacha20-ietf-poly1305 = AEAD_XCHACHA20_POLY1305
|
||||
Plain: NONE
|
||||
|
||||
Available forward strategies:
|
||||
rr: Round Robin mode
|
||||
ha: High Availability mode
|
||||
--
|
||||
SSH scheme:
|
||||
ssh://user[:pass]@host:port[?key=keypath&timeout=SECONDS]
|
||||
timeout: timeout of ssh handshake and channel operation, default: 5
|
||||
|
||||
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...
|
||||
--
|
||||
SSR scheme:
|
||||
ssr://method:pass@host:port?protocol=xxx&protocol_param=yyy&obfs=zzz&obfs_param=xyz
|
||||
|
||||
--
|
||||
TLS client scheme:
|
||||
tls://host:port[?serverName=SERVERNAME][&skipVerify=true][&cert=PATH][&alpn=proto1][&alpn=proto2]
|
||||
|
||||
Proxy over tls client:
|
||||
tls://host:port[?skipVerify=true][&serverName=SERVERNAME],scheme://
|
||||
tls://host:port[?skipVerify=true],http://[user:pass@]
|
||||
tls://host:port[?skipVerify=true],socks5://[user:pass@]
|
||||
tls://host:port[?skipVerify=true],vmess://[security:]uuid@?alterID=num
|
||||
|
||||
TLS server scheme:
|
||||
tls://host:port?cert=PATH&key=PATH[&alpn=proto1][&alpn=proto2]
|
||||
|
||||
Proxy over tls server:
|
||||
tls://host:port?cert=PATH&key=PATH,scheme://
|
||||
tls://host:port?cert=PATH&key=PATH,http://
|
||||
tls://host:port?cert=PATH&key=PATH,socks5://
|
||||
tls://host:port?cert=PATH&key=PATH,ss://method:pass@
|
||||
|
||||
--
|
||||
Trojan client scheme:
|
||||
trojan://pass@host:port[?serverName=SERVERNAME][&skipVerify=true][&cert=PATH]
|
||||
trojanc://pass@host:port (cleartext, without TLS)
|
||||
|
||||
Trojan server scheme:
|
||||
trojan://pass@host:port?cert=PATH&key=PATH[&fallback=127.0.0.1]
|
||||
trojanc://pass@host:port[?fallback=127.0.0.1] (cleartext, without TLS)
|
||||
|
||||
--
|
||||
Unix domain socket scheme:
|
||||
unix://path
|
||||
|
||||
--
|
||||
VLESS scheme:
|
||||
vless://uuid@host:port[?fallback=127.0.0.1:80]
|
||||
|
||||
--
|
||||
VMess scheme:
|
||||
vmess://[security:]uuid@host:port[?alterID=num]
|
||||
if alterID=0 or not set, VMessAEAD will be enabled
|
||||
|
||||
Available security for vmess:
|
||||
zero, none, aes-128-gcm, chacha20-poly1305
|
||||
|
||||
--
|
||||
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]
|
||||
|
||||
Websocket server scheme:
|
||||
ws://:port[/path][?host=HOST]
|
||||
wss://:port[/path]?cert=PATH&key=PATH[?host=HOST]
|
||||
|
||||
Websocket with a specified proxy protocol:
|
||||
ws://host:port[/path][?host=HOST],scheme://
|
||||
ws://host:port[/path][?host=HOST],http://[user:pass@]
|
||||
ws://host:port[/path][?host=HOST],socks5://[user:pass@]
|
||||
|
||||
TLS and Websocket with a specified proxy protocol:
|
||||
tls://host:port[?skipVerify=true][&serverName=SERVERNAME],ws://[@/path[?host=HOST]],scheme://
|
||||
tls://host:port[?skipVerify=true],ws://[@/path[?host=HOST]],http://[user:pass@]
|
||||
tls://host:port[?skipVerify=true],ws://[@/path[?host=HOST]],socks5://[user:pass@]
|
||||
tls://host:port[?skipVerify=true],ws://[@/path[?host=HOST]],vmess://[security:]uuid@?alterID=num
|
||||
|
||||
--
|
||||
VM socket scheme(linux only):
|
||||
vsock://[CID]:port
|
||||
|
||||
if you want to listen on any address, just set CID to 4294967295.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Examples
|
||||
|
||||
<details>
|
||||
<summary><code>glider -example</code></summary>
|
||||
|
||||
```bash
|
||||
Examples:
|
||||
glider -config glider.conf
|
||||
-run glider with specified config file.
|
||||
|
||||
glider -config glider.conf -rulefile office.rule -rulefile home.rule
|
||||
-run glider with specified global config file and rule config files.
|
||||
glider -listen :8443 -verbose
|
||||
-listen on :8443, serve as http/socks5 proxy on the same port, in verbose mode.
|
||||
|
||||
glider -listen :8443
|
||||
-listen on :8443, serve as http/socks5 proxy on the same port.
|
||||
glider -listen socks5://:1080 -listen http://:8080 -verbose
|
||||
-multiple listeners: listen on :1080 as socks5 proxy server, and on :8080 as http proxy server.
|
||||
|
||||
glider -listen ss://AEAD_CHACHA20_POLY1305:pass@:8443
|
||||
-listen on 0.0.0.0:8443 as a ss server.
|
||||
glider -listen :8443 -forward direct://#interface=eth0 -forward direct://#interface=eth1
|
||||
-multiple forwarders: listen on 8443 and forward requests via interface eth0 and eth1 in round robin mode.
|
||||
|
||||
glider -listen socks5://:1080 -verbose
|
||||
-listen on :1080 as a socks5 proxy server, in verbose mode.
|
||||
glider -listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// -verbose
|
||||
-protocol chain: listen on :443 as a https(http over tls) proxy server.
|
||||
|
||||
glider -listen http://:8080 -forward socks5://127.0.0.1:1080
|
||||
-listen on :8080 as a http proxy server, forward all requests via socks5 server.
|
||||
glider -listen http://:8080 -forward socks5://serverA:1080,socks5://serverB:1080
|
||||
-proxy chain: listen on :8080 as a http proxy server, forward all requests via forward chain.
|
||||
|
||||
glider -listen redir://:1081 -forward ss://method:pass@1.1.1.1:8443
|
||||
-listen on :1081 as a transparent redirect server, forward all requests via remote ss server.
|
||||
glider -listen :8443 -forward socks5://serverA:1080 -forward socks5://serverB:1080#priority=10 -forward socks5://serverC:1080#priority=10
|
||||
-forwarder priority: serverA will only be used when serverB and serverC are not available.
|
||||
|
||||
glider -listen 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 tcp://:80 -forward tcp://serverA:80
|
||||
-tcp tunnel: listen on :80 and forward all requests to serverA:80.
|
||||
|
||||
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 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 -listen dnstun://:53=8.8.8.8:53 -forward ss://method:pass@server1:port1,ss://method:pass@server2:port2
|
||||
-listen on :1081 as transparent redirect server, :53 as dns server, use forward chain: server1 -> server2.
|
||||
|
||||
glider -listen socks5://:1080 -forward ss://method:pass@server1:port1 -forward ss://method:pass@server2:port2 -strategy rr
|
||||
-listen on :1080 as socks5 server, forward requests via server1 and server2 in roundrbin mode.
|
||||
glider -verbose -dns=:53 -dnsserver=8.8.8.8:53 -forward socks5://serverA:1080 -dnsrecord=abc.com/1.2.3.4
|
||||
-dns over proxy: listen on :53 as dns server, forward to 8.8.8.8:53 via socks5 server.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Config
|
||||
|
||||
```bash
|
||||
glider -config CONFIG_PATH
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
- [ConfigFile](config)
|
||||
- [glider.conf.example](config/glider.conf.example)
|
||||
- [office.rule.example](config/rules.d/office.rule.example)
|
||||
@ -176,9 +434,109 @@ Examples:
|
||||
- [transparent proxy without dnsmasq](config/examples/9.transparent_proxy_without_dnsmasq)
|
||||
|
||||
## Service
|
||||
- systemd: [https://github.com/nadoo/glider/blob/master/systemd/](https://github.com/nadoo/glider/blob/master/systemd/)
|
||||
|
||||
- dhcpd / dhcpd-failover:
|
||||
- service=dhcpd,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
|
||||
- service=dhcpd,eth1,192.168.1.100,192.168.1.199,720,fc:23:34:9e:25:01=192.168.1.101
|
||||
- service=dhcpd-failover,eth2,192.168.2.100,192.168.2.199,720
|
||||
- note: `dhcpd-failover` only serves requests when there's no other dhcp server exists in lan
|
||||
- detect interval: 1min
|
||||
|
||||
## Linux Daemon
|
||||
|
||||
- systemd: [https://github.com/nadoo/glider/tree/main/systemd](https://github.com/nadoo/glider/tree/main/systemd)
|
||||
|
||||
- <details> <summary>docker: click to see details</summary>
|
||||
|
||||
- run glider (config file path: /etc/glider/glider.conf)
|
||||
```
|
||||
docker run -d --name glider --net host --restart=always \
|
||||
-v /etc/glider:/etc/glider \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
nadoo/glider -config=/etc/glider/glider.conf
|
||||
```
|
||||
- run watchtower if you need auto update
|
||||
```
|
||||
docker run -d --name watchtower --restart=always \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
containrrr/watchtower --interval 21600 --cleanup \
|
||||
glider
|
||||
```
|
||||
- open udp ports if you need udp nat fullcone
|
||||
```
|
||||
iptables -I INPUT -p udp -m udp --dport 1024:65535 -j ACCEPT
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Customize Build
|
||||
|
||||
<details><summary>You can customize and build glider if you want a smaller binary (click to see details)</summary>
|
||||
|
||||
|
||||
1. Clone the source code:
|
||||
```bash
|
||||
git clone https://github.com/nadoo/glider && cd glider
|
||||
```
|
||||
2. Customize features:
|
||||
|
||||
```bash
|
||||
open `feature.go` & `feature_linux.go`, comment out the packages you don't need
|
||||
// _ "github.com/nadoo/glider/proxy/kcp"
|
||||
```
|
||||
|
||||
3. Build it:
|
||||
```bash
|
||||
go build -v -ldflags "-s -w"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Proxy & Protocol Chains
|
||||
<details><summary>In glider, you can easily chain several proxy servers or protocols together (click to see details)</summary>
|
||||
|
||||
- Chain proxy servers:
|
||||
|
||||
```bash
|
||||
forward=http://1.1.1.1:80,socks5://2.2.2.2:1080,ss://method:pass@3.3.3.3:8443@
|
||||
```
|
||||
|
||||
- Chain protocols: https proxy (http over tls)
|
||||
|
||||
```bash
|
||||
forward=tls://server.com:443,http://
|
||||
```
|
||||
|
||||
- Chain protocols: vmess over ws over tls
|
||||
|
||||
```bash
|
||||
forward=tls://server.com:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
|
||||
```
|
||||
|
||||
- Chain protocols and servers:
|
||||
|
||||
``` bash
|
||||
forward=socks5://1.1.1.1:1080,tls://server.com:443,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2
|
||||
```
|
||||
|
||||
- Chain protocols in listener: https proxy server
|
||||
|
||||
``` bash
|
||||
listen=tls://:443?cert=crtFilePath&key=keyFilePath,http://
|
||||
```
|
||||
|
||||
- Chain protocols in listener: http over smux over websocket proxy server
|
||||
|
||||
``` bash
|
||||
listen=ws://:10000,smux://,http://
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Links
|
||||
- [go-ss2](https://github.com/shadowsocks/go-shadowsocks2): ss protocol support
|
||||
- [conflag](https://github.com/nadoo/conflag): command line and config file parse support
|
||||
- [ArchLinux](https://www.archlinux.org/packages/community/x86_64/glider): a great linux distribution with glider pre-built package
|
||||
|
||||
- [ipset](https://github.com/nadoo/ipset): netlink ipset package for Go.
|
||||
- [conflag](https://github.com/nadoo/conflag): a drop-in replacement for Go's standard flag package with config file support.
|
||||
- [ArchLinux](https://archlinux.org/packages/extra/x86_64/glider): a great linux distribution with glider pre-built package.
|
||||
- [urlencode](https://www.w3schools.com/tags/ref_urlencode.asp): you should encode special characters in scheme url. e.g., `@`->`%40`
|
||||
|
203
conf.go
203
conf.go
@ -1,203 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/nadoo/conflag"
|
||||
)
|
||||
|
||||
var flag = conflag.New()
|
||||
|
||||
var conf struct {
|
||||
Verbose bool
|
||||
Strategy string
|
||||
CheckWebSite string
|
||||
CheckDuration int
|
||||
Listen []string
|
||||
Forward []string
|
||||
RuleFile []string
|
||||
RulesDir string
|
||||
|
||||
DNS string
|
||||
DNSServer []string
|
||||
|
||||
IPSet string
|
||||
|
||||
rules []*RuleConf
|
||||
}
|
||||
|
||||
func confInit() {
|
||||
flag.BoolVar(&conf.Verbose, "verbose", false, "verbose mode")
|
||||
flag.StringVar(&conf.Strategy, "strategy", "rr", "forward strategy, default: rr")
|
||||
flag.StringVar(&conf.CheckWebSite, "checkwebsite", "www.apple.com", "proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80")
|
||||
flag.IntVar(&conf.CheckDuration, "checkduration", 30, "proxy check duration(seconds)")
|
||||
flag.StringSliceUniqVar(&conf.Listen, "listen", nil, "listen url, format: SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT")
|
||||
flag.StringSliceUniqVar(&conf.Forward, "forward", nil, "forward url, format: SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT[,SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT]")
|
||||
flag.StringSliceUniqVar(&conf.RuleFile, "rulefile", nil, "rule file path")
|
||||
flag.StringVar(&conf.RulesDir, "rules-dir", "", "rule file folder")
|
||||
|
||||
flag.StringVar(&conf.DNS, "dns", "", "dns forwarder server listen address")
|
||||
flag.StringSliceUniqVar(&conf.DNSServer, "dnsserver", []string{"8.8.8.8:53"}, "remote dns server")
|
||||
|
||||
flag.StringVar(&conf.IPSet, "ipset", "", "ipset name")
|
||||
|
||||
flag.Usage = usage
|
||||
err := flag.Parse()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
if len(conf.Listen) == 0 && conf.DNS == "" {
|
||||
flag.Usage()
|
||||
fmt.Fprintf(os.Stderr, "ERROR: listen url must be specified.\n")
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
// rulefiles
|
||||
for _, ruleFile := range conf.RuleFile {
|
||||
rule, err := NewRuleConfFromFile(ruleFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
conf.rules = append(conf.rules, rule)
|
||||
}
|
||||
|
||||
if conf.RulesDir != "" {
|
||||
conf.RulesDir = path.Join(flag.ConfDir(), conf.RulesDir)
|
||||
ruleFolderFiles, _ := listDir(conf.RulesDir, ".rule")
|
||||
|
||||
for _, ruleFile := range ruleFolderFiles {
|
||||
rule, err := NewRuleConfFromFile(ruleFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
conf.rules = append(conf.rules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// RuleConf , every ruleForwarder points to a rule file
|
||||
type RuleConf struct {
|
||||
name string
|
||||
|
||||
Forward []string
|
||||
Strategy string
|
||||
CheckWebSite string
|
||||
CheckDuration int
|
||||
|
||||
DNSServer []string
|
||||
IPSet string
|
||||
|
||||
Domain []string
|
||||
IP []string
|
||||
CIDR []string
|
||||
}
|
||||
|
||||
// NewRuleConfFromFile .
|
||||
func NewRuleConfFromFile(ruleFile string) (*RuleConf, error) {
|
||||
p := &RuleConf{name: ruleFile}
|
||||
|
||||
f := conflag.NewFromFile("rule", ruleFile)
|
||||
f.StringSliceUniqVar(&p.Forward, "forward", nil, "forward url, format: SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT[,SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT]")
|
||||
f.StringVar(&p.Strategy, "strategy", "rr", "forward strategy, default: rr")
|
||||
f.StringVar(&p.CheckWebSite, "checkwebsite", "www.apple.com", "proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80")
|
||||
f.IntVar(&p.CheckDuration, "checkduration", 30, "proxy check duration(seconds)")
|
||||
|
||||
f.StringSliceUniqVar(&p.DNSServer, "dnsserver", nil, "remote dns server")
|
||||
f.StringVar(&p.IPSet, "ipset", "", "ipset name")
|
||||
|
||||
f.StringSliceUniqVar(&p.Domain, "domain", nil, "domain")
|
||||
f.StringSliceUniqVar(&p.IP, "ip", nil, "ip")
|
||||
f.StringSliceUniqVar(&p.CIDR, "cidr", nil, "cidr")
|
||||
|
||||
err := f.Parse()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
||||
func usage() {
|
||||
app := os.Args[0]
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, "%s v%s usage:\n", app, VERSION)
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Available Schemas:\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, " redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules)\n")
|
||||
fmt.Fprintf(os.Stderr, " tcptun: a simple tcp tunnel\n")
|
||||
fmt.Fprintf(os.Stderr, " dnstun: listen on udp port and forward all dns requests to remote dns server via forwarders(tcp)\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Available schemas for different modes:\n")
|
||||
fmt.Fprintf(os.Stderr, " listen: mixed ss socks5 http redir tcptun dnstun\n")
|
||||
fmt.Fprintf(os.Stderr, " forward: ss socks5 http\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Available methods for ss:\n")
|
||||
fmt.Fprintf(os.Stderr, " "+ListCipher())
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " NOTE: chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Available forward strategies:\n")
|
||||
fmt.Fprintf(os.Stderr, " rr: Round Robin mode\n")
|
||||
fmt.Fprintf(os.Stderr, " ha: High Availability mode\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Config file format(see `"+app+".conf.example` as an example):\n")
|
||||
fmt.Fprintf(os.Stderr, " # COMMENT LINE\n")
|
||||
fmt.Fprintf(os.Stderr, " KEY=VALUE\n")
|
||||
fmt.Fprintf(os.Stderr, " KEY=VALUE\n")
|
||||
fmt.Fprintf(os.Stderr, " # KEY equals to command line flag name: listen forward strategy...\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Examples:\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -config glider.conf\n")
|
||||
fmt.Fprintf(os.Stderr, " -run glider with specified config file.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -config glider.conf -rulefile office.rule -rulefile home.rule\n")
|
||||
fmt.Fprintf(os.Stderr, " -run glider with specified global config file and rule config files.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -listen :8443\n")
|
||||
fmt.Fprintf(os.Stderr, " -listen on :8443, serve as http/socks5 proxy on the same port.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -listen ss://AEAD_CHACHA20_POLY1305:pass@:8443\n")
|
||||
fmt.Fprintf(os.Stderr, " -listen on 0.0.0.0:8443 as a ss server.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -listen socks5://:1080 -verbose\n")
|
||||
fmt.Fprintf(os.Stderr, " -listen on :1080 as a socks5 proxy server, in verbose mode.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -listen http://:8080 -forward socks5://127.0.0.1:1080\n")
|
||||
fmt.Fprintf(os.Stderr, " -listen on :8080 as a http proxy server, forward all requests via socks5 server.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -forward ss://method:pass@1.1.1.1:8443\n")
|
||||
fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote ss server.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -listen 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 socks5://:1080 -listen http://:8080 -forward ss://method:pass@1.1.1.1:8443\n")
|
||||
fmt.Fprintf(os.Stderr, " -listen on :1080 as socks5 server, :8080 as http proxy server, forward all requests via remote ss server.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -listen dnstun://:53=8.8.8.8:53 -forward ss://method:pass@server1:port1,ss://method:pass@server2:port2\n")
|
||||
fmt.Fprintf(os.Stderr, " -listen on :1081 as transparent redirect server, :53 as dns server, use forward chain: server1 -> server2.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, " "+app+" -listen socks5://:1080 -forward ss://method:pass@server1:port1 -forward ss://method:pass@server2:port2 -strategy rr\n")
|
||||
fmt.Fprintf(os.Stderr, " -listen on :1080 as socks5 server, forward requests via server1 and server2 in roundrbin mode.\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
254
config.go
Normal file
254
config.go
Normal file
@ -0,0 +1,254 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/nadoo/conflag"
|
||||
|
||||
"github.com/nadoo/glider/dns"
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
"github.com/nadoo/glider/rule"
|
||||
)
|
||||
|
||||
var flag = conflag.New()
|
||||
|
||||
// Config is global config struct.
|
||||
type Config struct {
|
||||
Verbose bool
|
||||
LogFlags int
|
||||
TCPBufSize int
|
||||
UDPBufSize int
|
||||
|
||||
Listens []string
|
||||
|
||||
Forwards []string
|
||||
Strategy rule.Strategy
|
||||
|
||||
RuleFiles []string
|
||||
RulesDir string
|
||||
|
||||
DNS string
|
||||
DNSConfig dns.Config
|
||||
|
||||
rules []*rule.Config
|
||||
|
||||
Services []string
|
||||
}
|
||||
|
||||
func parseConfig() *Config {
|
||||
conf := &Config{}
|
||||
|
||||
flag.SetOutput(os.Stdout)
|
||||
|
||||
scheme := flag.String("scheme", "", "show help message of proxy scheme, use 'all' to see all schemes")
|
||||
example := flag.Bool("example", false, "show usage examples")
|
||||
|
||||
flag.BoolVar(&conf.Verbose, "verbose", false, "verbose mode")
|
||||
flag.IntVar(&conf.LogFlags, "logflags", 19, "do not change it if you do not know what it is, ref: https://pkg.go.dev/log#pkg-constants")
|
||||
flag.IntVar(&conf.TCPBufSize, "tcpbufsize", 32768, "tcp buffer size in Bytes")
|
||||
flag.IntVar(&conf.UDPBufSize, "udpbufsize", 2048, "udp buffer size in Bytes")
|
||||
flag.StringSliceUniqVar(&conf.Listens, "listen", nil, "listen url, see the URL section below")
|
||||
|
||||
flag.StringSliceVar(&conf.Forwards, "forward", nil, "forward url, see the URL section below")
|
||||
flag.StringVar(&conf.Strategy.Strategy, "strategy", "rr", `rr: Round Robin mode
|
||||
ha: High Availability mode
|
||||
lha: Latency based High Availability mode
|
||||
dh: Destination Hashing mode`)
|
||||
flag.StringVar(&conf.Strategy.Check, "check", "http://www.msftconnecttest.com/connecttest.txt#expect=200",
|
||||
`check=tcp[://HOST:PORT]: tcp port connect check
|
||||
check=http://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
|
||||
check=https://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
|
||||
check=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, env vars: FORWARDER_ADDR,FORWARDER_URL
|
||||
check=disable: disable health check`)
|
||||
flag.IntVar(&conf.Strategy.CheckInterval, "checkinterval", 30, "fowarder check interval(seconds)")
|
||||
flag.IntVar(&conf.Strategy.CheckTimeout, "checktimeout", 10, "fowarder check timeout(seconds)")
|
||||
flag.IntVar(&conf.Strategy.CheckTolerance, "checktolerance", 0, "fowarder check tolerance(ms), switch only when new_latency < old_latency - tolerance, only used in lha mode")
|
||||
flag.IntVar(&conf.Strategy.CheckLatencySamples, "checklatencysamples", 10, "use the average latency of the latest N checks")
|
||||
flag.BoolVar(&conf.Strategy.CheckDisabledOnly, "checkdisabledonly", false, "check disabled fowarders only")
|
||||
flag.IntVar(&conf.Strategy.MaxFailures, "maxfailures", 3, "max failures to change forwarder status to disabled")
|
||||
flag.IntVar(&conf.Strategy.DialTimeout, "dialtimeout", 3, "dial timeout(seconds)")
|
||||
flag.IntVar(&conf.Strategy.RelayTimeout, "relaytimeout", 0, "relay timeout(seconds)")
|
||||
flag.StringVar(&conf.Strategy.IntFace, "interface", "", "source ip or source interface")
|
||||
|
||||
flag.StringSliceUniqVar(&conf.RuleFiles, "rulefile", nil, "rule file path")
|
||||
flag.StringVar(&conf.RulesDir, "rules-dir", "", "rule file folder")
|
||||
|
||||
// dns configs
|
||||
flag.StringVar(&conf.DNS, "dns", "", "local dns server listen address")
|
||||
flag.StringSliceUniqVar(&conf.DNSConfig.Servers, "dnsserver", []string{"8.8.8.8:53"}, "remote dns server address")
|
||||
flag.BoolVar(&conf.DNSConfig.AlwaysTCP, "dnsalwaystcp", false, "always use tcp to query upstream dns servers no matter there is a forwarder or not")
|
||||
flag.IntVar(&conf.DNSConfig.Timeout, "dnstimeout", 3, "timeout value used in multiple dnsservers switch(seconds)")
|
||||
flag.IntVar(&conf.DNSConfig.MaxTTL, "dnsmaxttl", 1800, "maximum TTL value for entries in the CACHE(seconds)")
|
||||
flag.IntVar(&conf.DNSConfig.MinTTL, "dnsminttl", 0, "minimum TTL value for entries in the CACHE(seconds)")
|
||||
flag.IntVar(&conf.DNSConfig.CacheSize, "dnscachesize", 4096, "max number of dns response in CACHE")
|
||||
flag.BoolVar(&conf.DNSConfig.CacheLog, "dnscachelog", false, "show query log of dns cache")
|
||||
flag.BoolVar(&conf.DNSConfig.NoAAAA, "dnsnoaaaa", false, "disable AAAA query")
|
||||
flag.StringSliceUniqVar(&conf.DNSConfig.Records, "dnsrecord", nil, "custom dns record, format: domain/ip")
|
||||
|
||||
// service configs
|
||||
flag.StringSliceUniqVar(&conf.Services, "service", nil, "run specified services, format: SERVICE_NAME[,SERVICE_CONFIG]")
|
||||
|
||||
flag.Usage = usage
|
||||
if err := flag.Parse(); err != nil {
|
||||
// flag.Usage()
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
if *scheme != "" {
|
||||
fmt.Fprint(flag.Output(), proxy.Usage(*scheme))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *example {
|
||||
fmt.Fprint(flag.Output(), examples)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// setup logger
|
||||
log.Set(conf.Verbose, conf.LogFlags)
|
||||
|
||||
if len(conf.Listens) == 0 && conf.DNS == "" && len(conf.Services) == 0 {
|
||||
// flag.Usage()
|
||||
fmt.Fprintf(os.Stderr, "ERROR: listen url must be specified.\n")
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
// tcpbufsize
|
||||
if conf.TCPBufSize > 0 {
|
||||
proxy.TCPBufSize = conf.TCPBufSize
|
||||
}
|
||||
|
||||
// udpbufsize
|
||||
if conf.UDPBufSize > 0 {
|
||||
proxy.UDPBufSize = conf.UDPBufSize
|
||||
}
|
||||
|
||||
loadRules(conf)
|
||||
return conf
|
||||
}
|
||||
|
||||
func loadRules(conf *Config) {
|
||||
// rulefiles
|
||||
for _, ruleFile := range conf.RuleFiles {
|
||||
if !path.IsAbs(ruleFile) {
|
||||
ruleFile = path.Join(flag.ConfDir(), ruleFile)
|
||||
}
|
||||
|
||||
rule, err := rule.NewConfFromFile(ruleFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
conf.rules = append(conf.rules, rule)
|
||||
}
|
||||
|
||||
if conf.RulesDir != "" {
|
||||
if !path.IsAbs(conf.RulesDir) {
|
||||
conf.RulesDir = path.Join(flag.ConfDir(), conf.RulesDir)
|
||||
}
|
||||
|
||||
ruleFolderFiles, _ := rule.ListDir(conf.RulesDir, ".rule")
|
||||
for _, ruleFile := range ruleFolderFiles {
|
||||
rule, err := rule.NewConfFromFile(ruleFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
conf.rules = append(conf.rules, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprint(flag.Output(), usage1)
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(flag.Output(), usage2, proxy.ServerSchemes(), proxy.DialerSchemes(), version)
|
||||
}
|
||||
|
||||
var usage1 = `
|
||||
Usage: glider [-listen URL]... [-forward URL]... [OPTION]...
|
||||
|
||||
e.g. glider -config /etc/glider/glider.conf
|
||||
glider -listen :8443 -forward socks5://serverA:1080 -forward socks5://serverB:1080 -verbose
|
||||
|
||||
OPTION:
|
||||
`
|
||||
|
||||
var usage2 = `
|
||||
URL:
|
||||
proxy: SCHEME://[USER:PASS@][HOST]:PORT
|
||||
chain: proxy,proxy[,proxy]...
|
||||
|
||||
e.g. -listen socks5://:1080
|
||||
-listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// (protocol chain)
|
||||
|
||||
e.g. -forward socks5://server:1080
|
||||
-forward tls://server.com:443,http:// (protocol chain)
|
||||
-forward socks5://serverA:1080,socks5://serverB:1080 (proxy chain)
|
||||
|
||||
SCHEME:
|
||||
listen : %s
|
||||
forward: %s
|
||||
|
||||
Note: use 'glider -scheme all' or 'glider -scheme SCHEME' to see help info for the scheme.
|
||||
|
||||
--
|
||||
Forwarder Options: FORWARD_URL#OPTIONS
|
||||
priority : the priority of that forwarder, the larger the higher, default: 0
|
||||
interface: the local interface or ip address used to connect remote server.
|
||||
|
||||
e.g. -forward socks5://server:1080#priority=100
|
||||
-forward socks5://server:1080#interface=eth0
|
||||
-forward socks5://server:1080#priority=100&interface=192.168.1.99
|
||||
|
||||
Services:
|
||||
dhcpd: service=dhcpd,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
|
||||
service=dhcpd-failover,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
|
||||
e.g. service=dhcpd,eth1,192.168.1.100,192.168.1.199,720
|
||||
|
||||
--
|
||||
Help:
|
||||
glider -help
|
||||
glider -scheme all
|
||||
glider -example
|
||||
|
||||
see README.md and glider.conf.example for more details.
|
||||
--
|
||||
glider %s, https://github.com/nadoo/glider (glider.proxy@gmail.com)
|
||||
`
|
||||
|
||||
var examples = `
|
||||
Examples:
|
||||
glider -config glider.conf
|
||||
-run glider with specified config file.
|
||||
|
||||
glider -listen :8443 -verbose
|
||||
-listen on :8443, serve as http/socks5 proxy on the same port, in verbose mode.
|
||||
|
||||
glider -listen socks5://:1080 -listen http://:8080 -verbose
|
||||
-multiple listeners: listen on :1080 as socks5 proxy server, and on :8080 as http proxy server.
|
||||
|
||||
glider -listen :8443 -forward direct://#interface=eth0 -forward direct://#interface=eth1
|
||||
-multiple forwarders: listen on 8443 and forward requests via interface eth0 and eth1 in round robin mode.
|
||||
|
||||
glider -listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// -verbose
|
||||
-protocol chain: listen on :443 as a https(http over tls) proxy server.
|
||||
|
||||
glider -listen http://:8080 -forward socks5://serverA:1080,socks5://serverB:1080
|
||||
-proxy chain: listen on :8080 as a http proxy server, forward all requests via forward chain.
|
||||
|
||||
glider -listen :8443 -forward socks5://serverA:1080 -forward socks5://serverB:1080#priority=10 -forward socks5://serverC:1080#priority=10
|
||||
-forwarder priority: serverA will only be used when serverB and serverC are not available.
|
||||
|
||||
glider -listen tcp://:80 -forward tcp://serverA:80
|
||||
-tcp tunnel: listen on :80 and forward all requests to serverA:80.
|
||||
|
||||
glider -listen udp://:53 -forward socks5://serverA:1080,udp://8.8.8.8:53
|
||||
-udp tunnel: listen on :53 and forward all udp requests to 8.8.8.8:53 via remote socks5 server.
|
||||
|
||||
glider -verbose -dns=:53 -dnsserver=8.8.8.8:53 -forward socks5://serverA:1080 -dnsrecord=abc.com/1.2.3.4
|
||||
-dns over proxy: listen on :53 as dns server, forward to 8.8.8.8:53 via socks5 server.
|
||||
`
|
@ -4,7 +4,15 @@ Command:
|
||||
```bash
|
||||
glider -config glider.conf
|
||||
```
|
||||
Config file, **just use the command line flag name as the key name**:
|
||||
Config file, **just use the command line flag name as key name**:
|
||||
```bash
|
||||
# COMMENT LINE
|
||||
KEY=VALUE
|
||||
KEY=VALUE
|
||||
# KEY equals to command line flag name: listen forward strategy...
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
### glider config file
|
||||
|
||||
@ -14,9 +22,6 @@ verbose
|
||||
# listen on 8443, serve as http/socks5 proxy on the same port.
|
||||
listen=:8443
|
||||
|
||||
# listen on udp port 5353, forward dns requests via tcp protocol
|
||||
listen=dnstun://:5353=8.8.8.8:53
|
||||
|
||||
# upstream forward proxy
|
||||
forward=socks5://192.168.1.10:1080
|
||||
|
||||
@ -26,37 +31,33 @@ forward=ss://method:pass@1.1.1.1:8443
|
||||
# upstream forward proxy (forward chain)
|
||||
forward=http://1.1.1.1:8080,socks5://2.2.2.2:1080
|
||||
|
||||
# multiple upstream proxies forwad strategy
|
||||
# multiple upstream proxies forward strategy
|
||||
strategy=rr
|
||||
|
||||
# Used to connect via forwarders, if the host is unreachable, the forwarder
|
||||
# will be set to disabled.
|
||||
# MUST be a HTTP website server address, format: HOST[:PORT]. HTTPS NOT SUPPORTED.
|
||||
checkwebsite=www.apple.com
|
||||
# forwarder health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
|
||||
# check duration
|
||||
checkduration=30
|
||||
# check interval
|
||||
checkinterval=30
|
||||
|
||||
|
||||
# Setup a dns forwarding server
|
||||
dns://53
|
||||
dns=:53
|
||||
# global remote dns server (you can specify different dns server in rule file)
|
||||
dnsserver=8.8.8.8:53
|
||||
|
||||
# Create and manage ipset on linux based on destinations in rule files
|
||||
# - add ip/cidrs in rule files on startup
|
||||
# - add resolved ips for domains in rule files by dns forwarder server
|
||||
# Usually used in transparent proxy mode on linux
|
||||
ipset=glider
|
||||
|
||||
# RULE FILES
|
||||
rules-dir=rules.d
|
||||
#rulefile=office.rule
|
||||
#rulefile=home.rule
|
||||
|
||||
# INCLUDE MORE CONFIG FILES
|
||||
#include=dnsrecord.inc.conf
|
||||
#include=more.inc.conf
|
||||
```
|
||||
See:
|
||||
- [glider.conf.example](config/glider.conf.example)
|
||||
- [examples](config/examples)
|
||||
- [glider.conf.example](glider.conf.example)
|
||||
- [examples](examples)
|
||||
|
||||
## Rule File
|
||||
Rule file, **same as the config file but specify forwarders based on destinations**:
|
||||
@ -66,12 +67,20 @@ forward=socks5://192.168.1.10:1080
|
||||
forward=ss://method:pass@1.1.1.1:8443
|
||||
forward=http://192.168.2.1:8080,socks5://192.168.2.2:1080
|
||||
strategy=rr
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
checkinterval=30
|
||||
|
||||
# DNS SERVER for domains in this rule file
|
||||
dnsserver=208.67.222.222:53
|
||||
|
||||
# IPSET MANAGEMENT
|
||||
# ----------------
|
||||
# Create and mange ipset on linux based on destinations in rule files
|
||||
# - add ip/cidrs in rule files on startup
|
||||
# - add resolved ips for domains in rule files by dns forwarding server
|
||||
# Usually used in transparent proxy mode on linux
|
||||
ipset=glider
|
||||
|
||||
# YOU CAN SPECIFY DESTINATIONS TO USE THE ABOVE FORWARDERS
|
||||
# matches abc.com and *.abc.com
|
||||
domain=abc.com
|
||||
|
7
config/dnsrecord.inc.conf.example
Normal file
7
config/dnsrecord.inc.conf.example
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
# intranet
|
||||
dnsrecord=oa.yourcompany.local/10.0.0.1
|
||||
dnsrecord=git.yourcompany.local/10.0.0.2
|
||||
|
||||
# ad
|
||||
#dnsrecord=ad.domain/127.0.0.1
|
@ -12,10 +12,8 @@ forward=http://1.1.1.1:8080
|
||||
# High Availability mode: ha
|
||||
strategy=rr
|
||||
|
||||
# Used to connect via forwarders, if the host is unreachable, the forwarder
|
||||
# will be set to disabled.
|
||||
# MUST be a HTTP website server address, format: HOST[:PORT]. HTTPS NOT SUPPORTED.
|
||||
checkwebsite=www.apple.com
|
||||
# forwarder health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
|
||||
# check duration(seconds)
|
||||
checkduration=30
|
||||
# check interval(seconds)
|
||||
checkinterval=30
|
||||
|
@ -8,8 +8,9 @@ forward=http://1.1.1.1:8080
|
||||
# High Availability mode: ha
|
||||
strategy=rr
|
||||
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
# forwarder health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
checkinterval=30
|
||||
|
||||
|
||||
# matches abc.com and *.abc.com
|
||||
|
@ -13,8 +13,9 @@ forward=http://1.1.1.1:8080
|
||||
# High Availability mode: ha
|
||||
strategy=rr
|
||||
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
# forwarder health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
checkinterval=30
|
||||
|
||||
|
||||
# NOTE HERE:
|
||||
|
@ -1,17 +1,18 @@
|
||||
|
||||
|
||||
forward=http://forwarder4:8080
|
||||
forward=http://forwarder1:8080
|
||||
|
||||
# first connect forwarder1 then forwarder2 then internet
|
||||
forward=http://forwarder5:8080,socks6://forwarder3:1080
|
||||
forward=http://forwarder1:8080,socks5://forwarder2:1080
|
||||
|
||||
|
||||
# Round Robin mode: rr
|
||||
# High Availability mode: ha
|
||||
strategy=rr
|
||||
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
# forwarder health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
checkinterval=30
|
||||
|
||||
|
||||
# matches 192.168.0.0/16
|
||||
|
@ -10,8 +10,9 @@ forward=http://forwarder2:8080,socks5://forwarder3:1080
|
||||
# High Availability mode: ha
|
||||
strategy=rr
|
||||
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
# forwarder health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
checkinterval=30
|
||||
|
||||
|
||||
# matches 172.16.0.0/24
|
||||
|
@ -1,22 +1,22 @@
|
||||
|
||||
## 8. Transparent Proxy with dnsmasq
|
||||
|
||||
#### Setup a redirect proxy and a dnstunnel with glider
|
||||
#### Setup a redirect proxy and a dns server with glider
|
||||
glider.conf
|
||||
```bash
|
||||
verbose=True
|
||||
listen=redir://:1081
|
||||
listen=dnstun://5353=8.8.8.8:53
|
||||
forward=http://forwarder1:8080,socks5://forwarder2:1080
|
||||
forward=http://1.1.1.1:8080
|
||||
dns=:5353
|
||||
dnsserver=8.8.8.8:53
|
||||
strategy=rr
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
checkinterval=30
|
||||
```
|
||||
|
||||
#### Create a ipset manually
|
||||
```bash
|
||||
ipset create myset hash:ip
|
||||
ipset create myset hash:net
|
||||
```
|
||||
|
||||
#### Config dnsmasq
|
||||
@ -32,7 +32,7 @@ ipset=/example4.com/myset
|
||||
#### Config iptables on your linux gateway
|
||||
```bash
|
||||
iptables -t nat -I PREROUTING -p tcp -m set --match-set myset dst -j REDIRECT --to-ports 1081
|
||||
iptables -t nat -I OUTPUT -p tcp -m set --match-set myset dst -j REDIRECT --to-ports 1081
|
||||
#iptables -t nat -I OUTPUT -p tcp -m set --match-set myset dst -j REDIRECT --to-ports 1081
|
||||
```
|
||||
|
||||
#### When client requests network, the whole process:
|
||||
|
@ -3,11 +3,15 @@
|
||||
verbose=True
|
||||
|
||||
listen=redir://:1081
|
||||
listen=dnstun://5353=8.8.8.8:53
|
||||
|
||||
forward=http://forwarder1:8080,socks5://forwarder2:1080
|
||||
forward=http://1.1.1.1:8080
|
||||
|
||||
dns=:5353
|
||||
dnsserver=8.8.8.8:53
|
||||
|
||||
|
||||
strategy=rr
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
# forwarder health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
checkinterval=30
|
||||
|
@ -10,6 +10,11 @@ PC Client -> Gateway with glider running(linux box) -> Upstream Forwarders -> In
|
||||
|
||||
so you don't need any dns server in your network.
|
||||
|
||||
#### Create a ipset manually
|
||||
```bash
|
||||
ipset create glider hash:net
|
||||
```
|
||||
|
||||
#### Glider Configuration
|
||||
##### glider.conf
|
||||
```bash
|
||||
@ -21,9 +26,7 @@ listen=redir://:1081
|
||||
# as a dns forwarding server
|
||||
dns=:53
|
||||
dnsserver=8.8.8.8:53
|
||||
|
||||
# as a ipset manager
|
||||
ipset=glider
|
||||
dnsserver=8.8.4.4:53
|
||||
|
||||
# specify rule files
|
||||
rules-dir=rules.d
|
||||
@ -35,12 +38,14 @@ rules-dir=rules.d
|
||||
forward=http://forwarder1:8080,socks5://forwarder2:1080
|
||||
forward=http://1.1.1.1:8080
|
||||
strategy=rr
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
|
||||
# specify a different dns server(if need)
|
||||
dnsserver=208.67.222.222:53
|
||||
|
||||
# as a ipset manager
|
||||
ipset=glider
|
||||
|
||||
# specify destinations
|
||||
include=office.list
|
||||
|
||||
@ -61,29 +66,36 @@ domain=mycompany.com
|
||||
domain=mycompany1.com
|
||||
ip=4.4.4.4
|
||||
ip=5.5.5.5
|
||||
cidr=cidr=172.16.101.0/24
|
||||
cidr=cidr=172.16.102.0/24
|
||||
cidr=172.16.101.0/24
|
||||
cidr=172.16.102.0/24
|
||||
```
|
||||
|
||||
#### Config iptables on your linux gateway
|
||||
#### Configure iptables on your linux gateway
|
||||
```bash
|
||||
iptables -t nat -I PREROUTING -p tcp -m set --match-set glider dst -j REDIRECT --to-ports 1081
|
||||
iptables -t nat -I OUTPUT -p tcp -m set --match-set glider dst -j REDIRECT --to-ports 1081
|
||||
```
|
||||
|
||||
#### Client DNS settings
|
||||
use the linux server's ip as your dns server
|
||||
#### Server DNS settings
|
||||
Set server's nameserver to glider:
|
||||
```bash
|
||||
echo nameserver 127.0.0.1 > /etc/resolv.conf
|
||||
```
|
||||
|
||||
#### Client settings
|
||||
Use the linux server's ip as your gateway.
|
||||
Use the linux server's ip as your dns server.
|
||||
|
||||
#### When client requesting to access http://example1.com (in office.rule), the whole process:
|
||||
DNS Resolving:
|
||||
1. client sends a udp dns request to linux server, and glider will receive the request(as it listen on default dns port :53)
|
||||
2. upstream dns server choice: glider will lookup it's rule config and find out the dns server to use for this domain(matched "example1.com" in office.rule, so 208.67.222.222:53 will be choosen)
|
||||
3. glider uses the forwarder in office.rule to ask 208.67.222.222:53 for the resolve answers
|
||||
4. glider updates it's office rule config, add the resolved ip address to it
|
||||
5. glider adds the resolved ip into ipset "glider", and return the dns answer to client
|
||||
1. client sends a udp dns request to linux server, and glider will receive the request(as it listens on the default dns port :53)
|
||||
2. upstream dns server choice: glider will lookup it's rule config and find out the dns server to use for this domain(matched "example1.com" in office.rule, so 208.67.222.222:53 will be chosen)
|
||||
3. glider uses the forwarder in office.rule to ask 208.67.222.222:53 for the resolve answers(dns over proxy).
|
||||
4. glider updates it's office rule config, adds the resolved ip address to it.
|
||||
5. glider adds the resolved ip into ipset "glider", and returns the dns answer to client.
|
||||
|
||||
Destination Accessing:
|
||||
1. client sends http request to the resolved ip of example1.com
|
||||
2. linux gateway server will get the request
|
||||
3. iptabes matches the ip in ipset "glider" and redirect this request to :1081(glider)
|
||||
4. glider finds the ip in office rule, and then choose a forwarder in office.rule to complete the request
|
||||
1. client sends http request to the resolved ip of example1.com.
|
||||
2. linux gateway server will get the request.
|
||||
3. iptables matches the ip in ipset "glider" and redirect this request to :1081(glider)
|
||||
4. glider finds the ip in office rule, and then choose a forwarder in office.rule to complete the request.
|
||||
|
@ -9,8 +9,5 @@ listen=redir://:1081
|
||||
dns=:53
|
||||
dnsserver=8.8.8.8:53
|
||||
|
||||
# as a ipset manager
|
||||
ipset=glider
|
||||
|
||||
# parse all *.rule files in rules.d folder
|
||||
rules-dir=rules.d
|
||||
|
@ -1,18 +1,21 @@
|
||||
|
||||
|
||||
forward=http://forwarder4:8080
|
||||
forward=http://forwarder1:8080
|
||||
|
||||
# first connect forwarder1 then forwarder2 then internet
|
||||
forward=http://forwarder5:8080,socks6://forwarder3:1080
|
||||
forward=http://forwarder1:8080,socks5://forwarder2:1080
|
||||
|
||||
|
||||
# Round Robin mode: rr
|
||||
# High Availability mode: ha
|
||||
strategy=rr
|
||||
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
# forwarder health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
checkinterval=30
|
||||
|
||||
# as a ipset manager
|
||||
ipset=glider
|
||||
|
||||
# matches 192.168.0.0/16
|
||||
cidr=192.168.0.0/16
|
||||
|
@ -3,5 +3,5 @@ domain=mycompany.com
|
||||
domain=mycompany1.com
|
||||
ip=4.4.4.4
|
||||
ip=5.5.5.5
|
||||
cidr=cidr=172.16.101.0/24
|
||||
cidr=cidr=172.16.102.0/24
|
||||
cidr=172.16.101.0/24
|
||||
cidr=172.16.102.0/24
|
||||
|
@ -9,14 +9,19 @@ forward=http://forwarder2:8080,socks5://forwarder3:1080
|
||||
# Round Robin mode: rr
|
||||
# High Availability mode: ha
|
||||
strategy=rr
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
# forwarder health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
checkinterval=30
|
||||
|
||||
# specify a different dns server(if need)
|
||||
dnsserver=208.67.222.222:53
|
||||
|
||||
# as a ipset manager
|
||||
ipset=glider
|
||||
|
||||
# specify destinations
|
||||
#include=office.list
|
||||
include=office.list
|
||||
|
||||
domain=example1.com
|
||||
domain=example2.com
|
||||
# matches ip
|
||||
|
@ -32,42 +32,131 @@ verbose=True
|
||||
# different protocols.
|
||||
|
||||
# listen on 8443, serve as http/socks5 proxy on the same port.
|
||||
listen=:8443
|
||||
# listen=:8443
|
||||
listen=127.0.0.1:8443
|
||||
|
||||
# listen on 8448 as a ss server.
|
||||
# listen=ss://AEAD_CHACHA20_POLY1305:pass@:8448
|
||||
|
||||
# listen on 8080 as a http proxy server.
|
||||
listen=http://:8080
|
||||
# listen=http://:8080
|
||||
|
||||
# listen on 1080 as a socks5 proxy server.
|
||||
listen=socks5://:1080
|
||||
# listen=socks5://:1080
|
||||
|
||||
# listen on 1234 as vless proxy server.
|
||||
# listen=vless://uuid@:1234
|
||||
# listen on 1234 as vless proxy server, fallback to 127.0.0.1:8080 http server when client auth failed.
|
||||
# listen=vless://uuid@:1234?fallback=127.0.0.1:8080
|
||||
|
||||
# listen on 1081 as a linux transparent proxy server.
|
||||
# listen=redir://:1081
|
||||
|
||||
# listen on 1082 as a tcp tunnel, all requests to :1082 will be forward to 1.1.1.1:80
|
||||
# listen=tcptun://:1082=1.1.1.1:80
|
||||
# listen on 1082 as a linux transparent proxy server(tproxy).
|
||||
# listen=tproxy://:1082
|
||||
|
||||
# a dnstun is a special dns forwarder server with a fixed remote dns
|
||||
# listen on udp port 5353, act as a local dns server,
|
||||
# forward all requests to 8.8.8.8:53 via tcp protocol
|
||||
# listen=dnstun://:5353=8.8.8.8:53
|
||||
# http over tls (HTTPS proxy)
|
||||
# listen=tls://:443?cert=crtFilePath&key=keyFilePath,http://
|
||||
|
||||
# ss over tls
|
||||
# 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://
|
||||
|
||||
# socks5 over kcp
|
||||
# listen=kcp://aes:key@127.0.0.1:8444?dataShards=10&parityShards=3&mode=fast,socks5://
|
||||
|
||||
# vless server
|
||||
# listen=vless://UUID@:1234
|
||||
|
||||
# vless over tls server
|
||||
# listen=tls://:1234?cert=/path/to/cert&key=/path/to/key,vless://UUID@?fallback=127.0.0.1:80
|
||||
|
||||
# vless over ws
|
||||
# listen=ws://:1234/path?host=domain.com,vless://707f20ea-d4b8-4d1d-8e2e-2c86cb2ed97a@?fallback=127.0.0.1:80
|
||||
|
||||
# trojan server
|
||||
# listen=trojan://PASSWORD@:1234?cert=/path/to/cert&key=/path/to/key&fallback=127.0.0.1
|
||||
|
||||
# trojanc server (trojan without tls)
|
||||
# listen=trojanc://PASSWORD@:1234?fallback=127.0.0.1
|
||||
|
||||
# FORWARDERS
|
||||
# ----------
|
||||
# Forwarders, we can setup multiple forwarders.
|
||||
# forward=SCHEME#OPTIONS
|
||||
|
||||
# FORWARDER OPTIONS
|
||||
# priority: set the priority of that forwarder, default:0
|
||||
# interface: set local interface or ip address used to connect remote server
|
||||
|
||||
# Socks5 proxy as forwarder
|
||||
# forward=socks5://192.168.1.10:1080
|
||||
|
||||
# Socks5 proxy as forwarder with priority 100
|
||||
# forward=socks5://192.168.1.10:1080#priority=100
|
||||
|
||||
# Socks5 proxy as forwarder with priority 100 and use `eth0` as source interface
|
||||
# forward=socks5://192.168.1.10:1080#priority=100&interface=eth0
|
||||
|
||||
# Socks5 proxy as forwarder with priority 100 and use `192.168.1.100` as source ip
|
||||
# forward=socks5://192.168.1.10:1080#priority=100&interface=192.168.1.100
|
||||
|
||||
# SS proxy as forwarder
|
||||
# forward=ss://method:pass@1.1.1.1:8443
|
||||
|
||||
# SSR proxy as forwarder
|
||||
# forward=ssr://method:pass@1.1.1.1:8443?protocol=auth_aes128_md5&protocol_param=xxx&obfs=tls1.2_ticket_auth&obfs_param=yyy
|
||||
|
||||
# ssh forwarder
|
||||
# forward=ssh://user[:pass]@host:port[?key=keypath&timeout=SECONDS]
|
||||
# forward=ssh://root:pass@host:port
|
||||
# forward=ssh://root@host:port?key=/path/to/keyfile
|
||||
# forward=ssh://root@host:port?key=/path/to/keyfile&timeout=5
|
||||
|
||||
# http proxy as forwarder
|
||||
# forward=http://1.1.1.1:8080
|
||||
|
||||
# trojan as forwarder
|
||||
# forward=trojan://PASSWORD@1.1.1.1:8080[?serverName=SERVERNAME][&skipVerify=true]
|
||||
|
||||
# trojanc as forwarder
|
||||
# forward=trojanc://PASSWORD@1.1.1.1:8080
|
||||
|
||||
# vless forwarder
|
||||
# forward=vless://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443
|
||||
|
||||
# vmess with aead auth
|
||||
# forward=vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443
|
||||
|
||||
# vmess with md5 auth (by setting alterID)
|
||||
# forward=vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443?alterID=2
|
||||
|
||||
# vmess over tls
|
||||
# forward=tls://server.com:443,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98
|
||||
|
||||
# vmess over websocket
|
||||
# forward=ws://1.1.1.1:80/path?host=server.com,vmess://chacha20-poly1305:5a146038-0b56-4e95-b1dc-5c6f5a32cd98@
|
||||
|
||||
# vmess over ws over tls
|
||||
# forward=tls://server.com:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98
|
||||
# forward=tls://server.com:443,ws://@/path,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98
|
||||
|
||||
# ss over tls
|
||||
# forward=tls://server.com:443,ss://AEAD_CHACHA20_POLY1305:pass@
|
||||
|
||||
# ss over kcp
|
||||
# forward=kcp://aes:key@127.0.0.1:8444?dataShards=10&parityShards=3&mode=fast,ss://AEAD_CHACHA20_POLY1305:pass@
|
||||
|
||||
# ss with simple-obfs
|
||||
# forward=simple-obfs://1.1.1.1:443?type=tls&host=apple.com,ss://AEAD_CHACHA20_POLY1305:pass@
|
||||
|
||||
# socks5 over unix domain socket
|
||||
# forward=unix:///dev/shm/socket,socks5://
|
||||
|
||||
# FORWARDER CHAIN
|
||||
# ---------------
|
||||
@ -82,49 +171,127 @@ listen=socks5://:1080
|
||||
|
||||
# Round Robin mode: rr
|
||||
# High Availability mode: ha
|
||||
# Latency based High Availability mode: lha
|
||||
# Destination Hashing mode: dh
|
||||
strategy=rr
|
||||
|
||||
# FORWARDER SETTINGS
|
||||
# ------------------
|
||||
# We can set some parameters for forwarders.
|
||||
|
||||
# forwarder will be set to disabled on how many failures counted(both dial and relay).
|
||||
maxfailures=3
|
||||
|
||||
# timeout for create a connection(seconds)
|
||||
# dialtimeout=3
|
||||
|
||||
# timeout for relay data from proxy server and client(seconds)
|
||||
# DO NOT change it if you don't know what will happen.
|
||||
# relaytimeout=0
|
||||
|
||||
|
||||
# FORWARDERS CHECK
|
||||
# ----------------
|
||||
# We can check whether a forwarder is available.
|
||||
|
||||
# Used to connect via forwarders, if the host is unreachable, the forwarder
|
||||
# will be set to disabled.
|
||||
# MUST be a HTTP website server address, format: HOST[:PORT]. HTTPS NOT SUPPORTED.
|
||||
checkwebsite=www.apple.com
|
||||
# Forwarder health check:
|
||||
# check=tcp[://HOST:PORT]: tcp port connect check
|
||||
# check=http://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
|
||||
# check=https://HOST[:PORT][/URI][#expect=REGEX_MATCH_IN_RESP_LINE]
|
||||
# e.g. check=https://www.netflix.com/title/81215567#expect=301|404
|
||||
# check=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, environment variables: FORWARDER_ADDR,FORWARDER_URL
|
||||
# check=disable: disable health check
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
|
||||
# check duration(seconds)
|
||||
checkduration=30
|
||||
# check interval(seconds)
|
||||
checkinterval=30
|
||||
|
||||
# timeout to set a forwarder to be disabled(seconds)
|
||||
checktimeout=10
|
||||
|
||||
# switch forwarder only when new_latency < old_latency - tolerance, used in lha mode
|
||||
checktolerance=100
|
||||
|
||||
# use the average latency of the latest N checks
|
||||
checklatencysamples=10
|
||||
|
||||
# check disabled fowarders only
|
||||
checkdisabledonly=false
|
||||
|
||||
# DNS FORWARDING SERVER
|
||||
# ----------------
|
||||
# A dns forwarding server listens on UDP and forward dns requests to remote dns server in TCP via forwarders
|
||||
# we can specify different upstream dns server in rule file for different destinations
|
||||
# we can specify different upstream dns server in rule file for different destinations.
|
||||
|
||||
# Setup a dns forwarding server
|
||||
dns=:53
|
||||
# dns=:53
|
||||
|
||||
# global remote dns server (you can specify different dns server in rule file)
|
||||
dnsserver=8.8.8.8:53
|
||||
dnsserver=1.1.1.1:53
|
||||
|
||||
# By default, when glider received udp dns request and there's no forwarder specified,
|
||||
# it will use udp to query upstream dns servers, otherwise, use tcp;
|
||||
# you can set dnsalwaystcp=true to always use tcp no matter there is a forwarder or not.
|
||||
# dnsalwaystcp=false
|
||||
|
||||
# IPSET MANAGEMENT
|
||||
# ----------------
|
||||
# Create and mange ipset on linux based on destinations in rule files
|
||||
# - add ip/cidrs in rule files on startup
|
||||
# - add resolved ips for domains in rule files by dns forwarding server
|
||||
# Usually used in transparent proxy mode on linux
|
||||
ipset=glider
|
||||
# timeout value used in multiple dnsservers switch(seconds)
|
||||
dnstimeout=3
|
||||
|
||||
# maximum TTL value for entries in the CACHE(seconds)
|
||||
dnsmaxttl=1800
|
||||
|
||||
# minimum TTL value for entries in the CACHE(seconds)
|
||||
dnsminttl=0
|
||||
|
||||
# size of CACHE
|
||||
dnscachesize=4096
|
||||
|
||||
# show query log of dns cache
|
||||
dnscachelog=True
|
||||
|
||||
# disable AAAA queries
|
||||
# dnsnoaaaa=True
|
||||
|
||||
# custom records
|
||||
dnsrecord=www.example.com/1.2.3.4
|
||||
dnsrecord=www.example.com/2606:2800:220:1:248:1893:25c8:1946
|
||||
|
||||
# SERVICES
|
||||
# service=dhcpd,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
|
||||
# service=dhcpd-failover,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
|
||||
# e.g.:
|
||||
# service=dhcpd,eth1,192.168.1.100,192.168.1.199,720
|
||||
# service=dhcpd,eth2,192.168.2.100,192.168.2.199,720,fc:23:34:9e:25:01=192.168.2.101,fc:23:34:9e:25:02=192.168.2.102
|
||||
|
||||
# INTERFACE SPECIFIC
|
||||
# ------------------
|
||||
# Specify global outbound ip/interface.
|
||||
#
|
||||
# interface=""
|
||||
# interface="192.168.1.100"
|
||||
# interface="eth0"
|
||||
#
|
||||
# Specify interface for a forwarder:
|
||||
# forward=socks5://192.168.1.10:1080#priority=100&interface=eth0
|
||||
# forward=socks5://192.168.1.10:1080#priority=100&interface=192.168.1.100
|
||||
|
||||
# RULE FILES
|
||||
# ----------
|
||||
# Specify additional forward rules
|
||||
|
||||
# Specify additional forward rules.
|
||||
#
|
||||
# specify rules folder, so all *.rule files under this folder will be parsed as rule file
|
||||
rules-dir=rules.d
|
||||
|
||||
# rules-dir=rules.d
|
||||
#
|
||||
# specify a rule file
|
||||
#rulefile=office.rule
|
||||
#rulefile=home.rule
|
||||
|
||||
# INCLUDE CONFIG FILES
|
||||
# ----------
|
||||
#include=dnsrecord.inc.conf
|
||||
#include=more.conf
|
||||
|
||||
# ENVIRONMENT VARIABLES
|
||||
# ----------
|
||||
# use {$ENV_VAR_NAME} in VALUE to get the Environment Variable value.
|
||||
# forward=socks5://{$USER_NAME}:{$USER_PASS}@:1080
|
||||
|
@ -14,15 +14,20 @@ forward=http://192.168.2.1:8080,socks5://192.168.2.2:1080
|
||||
strategy=rr
|
||||
|
||||
# FORWARDER CHECK SETTINGS
|
||||
checkwebsite=www.apple.com
|
||||
checkduration=30
|
||||
check=http://www.msftconnecttest.com/connecttest.txt#expect=200
|
||||
checkinterval=30
|
||||
|
||||
# DNS SERVER for domains in this rule file
|
||||
dnsserver=208.67.222.222:53
|
||||
|
||||
# IPSET
|
||||
# specify a ipset for destinations in this rule file
|
||||
#ipset=office
|
||||
# IPSET MANAGEMENT
|
||||
# ----------------
|
||||
# Create and mange ipset on linux based on destinations in rule files
|
||||
# - add ip/cidrs in rule files on startup
|
||||
# - add resolved ips for domains in rule files by dns forwarding server
|
||||
# Usually used in transparent proxy mode on linux
|
||||
# Note: this will create 2 ipsets, glider for ipv4 and glider6 for ipv6
|
||||
ipset=glider
|
||||
|
||||
# DESTINATIONS
|
||||
# ------------
|
||||
@ -30,7 +35,7 @@ dnsserver=208.67.222.222:53
|
||||
|
||||
# INCLUDE FILE
|
||||
# we can include a list file with only destinations settings
|
||||
include=office.list.example
|
||||
include=office.list
|
||||
|
||||
# matches example.com and *.example.com
|
||||
domain=example.com
|
||||
|
7
config/rules.d/reject.rule.example
Normal file
7
config/rules.d/reject.rule.example
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
forward=reject://
|
||||
|
||||
ipset=glider
|
||||
|
||||
domain=pornhub.com
|
||||
domain=amazon.com
|
54
conn.go
54
conn.go
@ -1,54 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type conn struct {
|
||||
r *bufio.Reader
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func newConn(c net.Conn) conn {
|
||||
return conn{bufio.NewReader(c), c}
|
||||
}
|
||||
|
||||
func newConnSize(c net.Conn, n int) conn {
|
||||
return conn{bufio.NewReaderSize(c, n), c}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
52
dialer.go
52
dialer.go
@ -1,52 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// A Dialer means to establish a connection and relay it.
|
||||
type Dialer interface {
|
||||
// Addr()
|
||||
Addr() string
|
||||
|
||||
// Dial connects to the given address via the proxy.
|
||||
Dial(network, addr string) (c net.Conn, err error)
|
||||
|
||||
// Get the dialer by dstAddr
|
||||
NextDialer(dstAddr string) Dialer
|
||||
}
|
||||
|
||||
// DialerFromURL parses url and get a Proxy
|
||||
// TODO: table
|
||||
func DialerFromURL(s string, cDialer Dialer) (Dialer, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
logf("parse err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := u.Host
|
||||
var user, pass string
|
||||
if u.User != nil {
|
||||
user = u.User.Username()
|
||||
pass, _ = u.User.Password()
|
||||
}
|
||||
|
||||
if cDialer == nil {
|
||||
cDialer = Direct
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
return NewHTTP(addr, user, pass, cDialer, nil)
|
||||
case "socks5":
|
||||
return NewSOCKS5(addr, user, pass, cDialer, nil)
|
||||
case "ss":
|
||||
p, err := NewSS(addr, user, pass, cDialer, nil)
|
||||
return p, err
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown schema '" + u.Scheme + "'")
|
||||
}
|
22
direct.go
22
direct.go
@ -1,22 +0,0 @@
|
||||
package main
|
||||
|
||||
import "net"
|
||||
|
||||
// direct proxy
|
||||
type direct struct {
|
||||
}
|
||||
|
||||
// Direct proxy
|
||||
var Direct = &direct{}
|
||||
|
||||
func (d *direct) Addr() string { return "DIRECT" }
|
||||
|
||||
func (d *direct) Dial(network, addr string) (net.Conn, error) {
|
||||
c, err := net.Dial(network, addr)
|
||||
if c, ok := c.(*net.TCPConn); ok {
|
||||
c.SetKeepAlive(true)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (d *direct) NextDialer(dstAddr string) Dialer { return d }
|
252
dns.go
252
dns.go
@ -1,252 +0,0 @@
|
||||
// https://tools.ietf.org/html/rfc1035
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DNSUDPHeaderLen is the length of UDP dns msg header
|
||||
const DNSUDPHeaderLen = 12
|
||||
|
||||
// DNSTCPHeaderLen is the length of TCP dns msg header
|
||||
const DNSTCPHeaderLen = 2 + DNSUDPHeaderLen
|
||||
|
||||
// DNSUDPMaxLen is the max size of udp dns request.
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.2.1
|
||||
// Messages carried by UDP are restricted to 512 bytes (not counting the IP
|
||||
// or UDP headers). Longer messages are truncated and the TC bit is set in
|
||||
// the header.
|
||||
// TODO: If the request length > 512 then the client will send TCP packets instead,
|
||||
// so we should also serve tcp requests.
|
||||
const DNSUDPMaxLen = 512
|
||||
|
||||
// DNSQueryTypeA ipv4
|
||||
const DNSQueryTypeA = 1
|
||||
|
||||
// DNSQueryTypeAAAA ipv6
|
||||
const DNSQueryTypeAAAA = 28
|
||||
|
||||
type dnsQuery struct {
|
||||
DomainName string
|
||||
QueryType uint16
|
||||
QueryClass uint16
|
||||
Offset int
|
||||
}
|
||||
|
||||
type dnsAnswer struct {
|
||||
// DomainName string
|
||||
QueryType uint16
|
||||
QueryClass uint16
|
||||
TTL uint32
|
||||
DataLength uint16
|
||||
Data []byte
|
||||
|
||||
IP string
|
||||
}
|
||||
|
||||
// DNSAnswerHandler .
|
||||
type DNSAnswerHandler func(domain, ip string) error
|
||||
|
||||
// DNS .
|
||||
type DNS struct {
|
||||
*Forwarder // as proxy client
|
||||
sDialer Dialer // dialer for server
|
||||
|
||||
dnsServer string
|
||||
|
||||
dnsServerMap map[string]string
|
||||
answerHandlers []DNSAnswerHandler
|
||||
}
|
||||
|
||||
// NewDNS returns a dns forwarder. client[dns.udp] -> glider[tcp] -> forwarder[dns.tcp] -> remote dns addr
|
||||
func NewDNS(addr, raddr string, sDialer Dialer) (*DNS, error) {
|
||||
s := &DNS{
|
||||
Forwarder: NewForwarder(addr, nil),
|
||||
sDialer: sDialer,
|
||||
|
||||
dnsServer: raddr,
|
||||
dnsServerMap: make(map[string]string),
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ListenAndServe .
|
||||
func (s *DNS) ListenAndServe() {
|
||||
c, err := net.ListenPacket("udp", s.addr)
|
||||
if err != nil {
|
||||
logf("failed to listen on %s: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
logf("listening UDP on %s", s.addr)
|
||||
|
||||
for {
|
||||
data := make([]byte, DNSUDPMaxLen)
|
||||
|
||||
n, clientAddr, err := c.ReadFrom(data)
|
||||
if err != nil {
|
||||
logf("DNS local read error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
data = data[:n]
|
||||
|
||||
go func() {
|
||||
query := parseQuery(data)
|
||||
domain := query.DomainName
|
||||
|
||||
dnsServer := s.GetServer(domain)
|
||||
|
||||
rc, err := s.sDialer.NextDialer(domain+":53").Dial("tcp", dnsServer)
|
||||
if err != nil {
|
||||
logf("failed to connect to server %v: %v", dnsServer, err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// 2 bytes length after tcp header, before dns message
|
||||
reqLen := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(reqLen, uint16(len(data)))
|
||||
rc.Write(reqLen)
|
||||
rc.Write(data)
|
||||
|
||||
// fmt.Printf("dns req len %d:\n%s\n\n", reqLen, hex.Dump(data[:]))
|
||||
|
||||
var respLen uint16
|
||||
err = binary.Read(rc, binary.BigEndian, &respLen)
|
||||
if err != nil {
|
||||
logf("proxy-dns: error in read respLen %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
respMsg := make([]byte, respLen)
|
||||
_, err = io.ReadFull(rc, respMsg)
|
||||
if err != nil {
|
||||
logf("proxy-dns: error in read respMsg %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// fmt.Printf("dns resp len %d:\n%s\n\n", respLen, hex.Dump(respMsg[:]))
|
||||
|
||||
var ip string
|
||||
// length is not needed in udp dns response. (2 bytes)
|
||||
// SEE RFC1035, section 4.2.2 TCP: The message is prefixed with a two byte length field which gives the message length, excluding the two byte length field.
|
||||
if respLen > 0 {
|
||||
query := parseQuery(respMsg)
|
||||
if len(respMsg) > query.Offset {
|
||||
answers := parseAnswers(respMsg[query.Offset:])
|
||||
for _, answer := range answers {
|
||||
if answer.IP != "" {
|
||||
ip += answer.IP + ","
|
||||
}
|
||||
|
||||
for _, h := range s.answerHandlers {
|
||||
h(query.DomainName, answer.IP)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_, err = c.WriteTo(respMsg, clientAddr)
|
||||
if err != nil {
|
||||
logf("error in local write: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
logf("proxy-dns %s <-> %s, %s: %s", clientAddr.String(), dnsServer, domain, ip)
|
||||
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// SetServer .
|
||||
func (s *DNS) SetServer(domain, server string) {
|
||||
s.dnsServerMap[domain] = server
|
||||
}
|
||||
|
||||
// GetServer .
|
||||
func (s *DNS) GetServer(domain string) string {
|
||||
|
||||
domainParts := strings.Split(domain, ".")
|
||||
length := len(domainParts)
|
||||
for i := length - 2; i >= 0; i-- {
|
||||
domain := strings.Join(domainParts[i:length], ".")
|
||||
|
||||
if server, ok := s.dnsServerMap[domain]; ok {
|
||||
return server
|
||||
}
|
||||
}
|
||||
|
||||
return s.dnsServer
|
||||
}
|
||||
|
||||
// AddAnswerHandler .
|
||||
func (s *DNS) AddAnswerHandler(h DNSAnswerHandler) {
|
||||
s.answerHandlers = append(s.answerHandlers, h)
|
||||
}
|
||||
|
||||
func parseQuery(p []byte) *dnsQuery {
|
||||
q := &dnsQuery{}
|
||||
|
||||
var i int
|
||||
var domain []byte
|
||||
for i = DNSUDPHeaderLen; i < len(p); {
|
||||
l := int(p[i])
|
||||
|
||||
if l == 0 {
|
||||
i++
|
||||
break
|
||||
}
|
||||
|
||||
domain = append(domain, p[i+1:i+l+1]...)
|
||||
domain = append(domain, '.')
|
||||
|
||||
i = i + l + 1
|
||||
}
|
||||
|
||||
q.DomainName = string(domain[:len(domain)-1])
|
||||
q.QueryType = binary.BigEndian.Uint16(p[i:])
|
||||
q.QueryClass = binary.BigEndian.Uint16(p[i+2:])
|
||||
q.Offset = i + 4
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func parseAnswers(p []byte) []*dnsAnswer {
|
||||
var answers []*dnsAnswer
|
||||
|
||||
for i := 0; i < len(p); {
|
||||
l := int(p[i])
|
||||
|
||||
if l == 0 {
|
||||
i++
|
||||
break
|
||||
}
|
||||
|
||||
answer := &dnsAnswer{}
|
||||
answer.QueryType = binary.BigEndian.Uint16(p[i+2:])
|
||||
answer.QueryClass = binary.BigEndian.Uint16(p[i+4:])
|
||||
answer.TTL = binary.BigEndian.Uint32(p[i+6:])
|
||||
answer.DataLength = binary.BigEndian.Uint16(p[i+10:])
|
||||
answer.Data = p[i+12 : i+12+int(answer.DataLength)]
|
||||
|
||||
if answer.QueryType == DNSQueryTypeA {
|
||||
answer.IP = net.IP(answer.Data[:net.IPv4len]).String()
|
||||
} else if answer.QueryType == DNSQueryTypeAAAA {
|
||||
answer.IP = net.IP(answer.Data[:net.IPv6len]).String()
|
||||
}
|
||||
|
||||
answers = append(answers, answer)
|
||||
|
||||
i = i + 12 + int(answer.DataLength)
|
||||
}
|
||||
|
||||
return answers
|
||||
}
|
127
dns/cache.go
Normal file
127
dns/cache.go
Normal file
@ -0,0 +1,127 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LruCache is the struct of LruCache.
|
||||
type LruCache struct {
|
||||
mu sync.Mutex
|
||||
size int
|
||||
head *item
|
||||
tail *item
|
||||
cache map[string]*item
|
||||
store map[string][]byte
|
||||
}
|
||||
|
||||
// item is the struct of cache item.
|
||||
type item struct {
|
||||
key string
|
||||
val []byte
|
||||
exp int64
|
||||
prev *item
|
||||
next *item
|
||||
}
|
||||
|
||||
// NewLruCache returns a new LruCache.
|
||||
func NewLruCache(size int) *LruCache {
|
||||
// init 2 items here, it doesn't matter cuz they will be deleted when the cache is full
|
||||
head, tail := &item{key: "head"}, &item{key: "tail"}
|
||||
head.next, tail.prev = tail, head
|
||||
c := &LruCache{
|
||||
size: size,
|
||||
head: head,
|
||||
tail: tail,
|
||||
cache: make(map[string]*item, size),
|
||||
store: make(map[string][]byte),
|
||||
}
|
||||
c.cache[head.key], c.cache[tail.key] = head, tail
|
||||
return c
|
||||
}
|
||||
|
||||
// Get gets an item from cache.
|
||||
func (c *LruCache) Get(k string) (v []byte, expired bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if v, ok := c.store[k]; ok {
|
||||
return v, false
|
||||
}
|
||||
|
||||
if it, ok := c.cache[k]; ok {
|
||||
v = it.val
|
||||
if it.exp < time.Now().Unix() {
|
||||
expired = true
|
||||
}
|
||||
c.moveToHead(it)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Set sets an item with key, value, and ttl(seconds).
|
||||
// if the ttl is zero, this item will be set and never be deleted.
|
||||
// if the key exists, update it with value and exp and move it to head.
|
||||
// if the key does not exist, put a new item to the cache's head.
|
||||
// finally, remove the tail if the cache is full.
|
||||
func (c *LruCache) Set(k string, v []byte, ttl int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if ttl == 0 {
|
||||
c.store[k] = v
|
||||
return
|
||||
}
|
||||
|
||||
exp := time.Now().Add(time.Second * time.Duration(ttl)).Unix()
|
||||
if it, ok := c.cache[k]; ok {
|
||||
it.val = v
|
||||
it.exp = exp
|
||||
c.moveToHead(it)
|
||||
return
|
||||
}
|
||||
|
||||
c.putToHead(k, v, exp)
|
||||
|
||||
// NOTE: the cache size will always >= 2,
|
||||
// but it doesn't matter in our environment.
|
||||
if len(c.cache) > c.size {
|
||||
c.removeTail()
|
||||
}
|
||||
}
|
||||
|
||||
// putToHead puts a new item to cache's head.
|
||||
func (c *LruCache) putToHead(k string, v []byte, exp int64) {
|
||||
it := &item{key: k, val: v, exp: exp, prev: nil, next: c.head}
|
||||
it.prev = nil
|
||||
it.next = c.head
|
||||
c.head.prev = it
|
||||
c.head = it
|
||||
|
||||
c.cache[k] = it
|
||||
}
|
||||
|
||||
// moveToHead moves an existing item to cache's head.
|
||||
func (c *LruCache) moveToHead(it *item) {
|
||||
if it != c.head {
|
||||
if c.tail == it {
|
||||
c.tail = it.prev
|
||||
c.tail.next = nil
|
||||
} else {
|
||||
it.prev.next = it.next
|
||||
it.next.prev = it.prev
|
||||
}
|
||||
it.prev = nil
|
||||
it.next = c.head
|
||||
c.head.prev = it
|
||||
c.head = it
|
||||
}
|
||||
}
|
||||
|
||||
// removeTail removes the tail from cache.
|
||||
func (c *LruCache) removeTail() {
|
||||
delete(c.cache, c.tail.key)
|
||||
|
||||
c.tail.prev.next = nil
|
||||
c.tail = c.tail.prev
|
||||
}
|
344
dns/client.go
Normal file
344
dns/client.go
Normal file
@ -0,0 +1,344 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// AnswerHandler function handles the dns TypeA or TypeAAAA answer.
|
||||
type AnswerHandler func(domain string, ip netip.Addr) error
|
||||
|
||||
// Config for dns.
|
||||
type Config struct {
|
||||
Servers []string
|
||||
Timeout int
|
||||
MaxTTL int
|
||||
MinTTL int
|
||||
Records []string
|
||||
AlwaysTCP bool
|
||||
CacheSize int
|
||||
CacheLog bool
|
||||
NoAAAA bool
|
||||
}
|
||||
|
||||
// Client is a dns client struct.
|
||||
type Client struct {
|
||||
proxy proxy.Proxy
|
||||
cache *LruCache
|
||||
config *Config
|
||||
upStream *UPStream
|
||||
upStreamMap map[string]*UPStream
|
||||
handlers []AnswerHandler
|
||||
}
|
||||
|
||||
// NewClient returns a new dns client.
|
||||
func NewClient(proxy proxy.Proxy, config *Config) (*Client, error) {
|
||||
c := &Client{
|
||||
proxy: proxy,
|
||||
cache: NewLruCache(config.CacheSize),
|
||||
config: config,
|
||||
upStream: NewUPStream(config.Servers),
|
||||
upStreamMap: make(map[string]*UPStream),
|
||||
}
|
||||
|
||||
// custom records
|
||||
for _, record := range config.Records {
|
||||
if err := c.AddRecord(record); err != nil {
|
||||
log.F("[dns] add record '%s' error: %s", record, err)
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Exchange handles request message and returns response message.
|
||||
// TODO: optimize it
|
||||
func (c *Client) Exchange(reqBytes []byte, clientAddr string, preferTCP bool) ([]byte, error) {
|
||||
req, err := UnmarshalMessage(reqBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.config.NoAAAA && req.Question.QTYPE == QTypeAAAA {
|
||||
respBytes := valCopy(reqBytes)
|
||||
respBytes[2] |= uint8(ResponseMsg) << 7
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
if req.Question.QTYPE == QTypeA || req.Question.QTYPE == QTypeAAAA {
|
||||
if v, expired := c.cache.Get(qKey(req.Question)); len(v) > 2 {
|
||||
v = valCopy(v)
|
||||
binary.BigEndian.PutUint16(v[:2], req.ID)
|
||||
|
||||
if c.config.CacheLog {
|
||||
log.F("[dns] %s <-> cache, type: %d, %s",
|
||||
clientAddr, req.Question.QTYPE, req.Question.QNAME)
|
||||
}
|
||||
|
||||
if expired { // update cache
|
||||
go func(qname string, reqBytes []byte, preferTCP bool) {
|
||||
defer pool.PutBuffer(reqBytes)
|
||||
if dnsServer, network, dialerAddr, respBytes, err := c.exchange(qname, reqBytes, preferTCP); err == nil {
|
||||
c.handleAnswer(respBytes, "cache", dnsServer, network, dialerAddr)
|
||||
}
|
||||
}(req.Question.QNAME, valCopy(reqBytes), preferTCP)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
dnsServer, network, dialerAddr, respBytes, err := c.exchange(req.Question.QNAME, reqBytes, preferTCP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Question.QTYPE != QTypeA && req.Question.QTYPE != QTypeAAAA {
|
||||
log.F("[dns] %s <-> %s(%s) via %s, type: %d, %s",
|
||||
clientAddr, dnsServer, network, dialerAddr, req.Question.QTYPE, req.Question.QNAME)
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
err = c.handleAnswer(respBytes, clientAddr, dnsServer, network, dialerAddr)
|
||||
return respBytes, err
|
||||
}
|
||||
|
||||
func (c *Client) handleAnswer(respBytes []byte, clientAddr, dnsServer, network, dialerAddr string) error {
|
||||
resp, err := UnmarshalMessage(respBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ips, ttl := c.extractAnswer(resp)
|
||||
if ttl > c.config.MaxTTL {
|
||||
ttl = c.config.MaxTTL
|
||||
} else if ttl < c.config.MinTTL {
|
||||
ttl = c.config.MinTTL
|
||||
}
|
||||
|
||||
if ttl <= 0 { // we got a null result
|
||||
ttl = 1800
|
||||
}
|
||||
|
||||
c.cache.Set(qKey(resp.Question), valCopy(respBytes), ttl)
|
||||
log.F("[dns] %s <-> %s(%s) via %s, %s/%d: %s, ttl: %ds",
|
||||
clientAddr, dnsServer, network, dialerAddr, resp.Question.QNAME, resp.Question.QTYPE, strings.Join(ips, ","), ttl)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) extractAnswer(resp *Message) ([]string, int) {
|
||||
var ips []string
|
||||
ttl := c.config.MinTTL
|
||||
for _, answer := range resp.Answers {
|
||||
if answer.TYPE == QTypeA || answer.TYPE == QTypeAAAA {
|
||||
if answer.IP.IsValid() && !answer.IP.IsUnspecified() {
|
||||
for _, h := range c.handlers {
|
||||
h(resp.Question.QNAME, answer.IP)
|
||||
}
|
||||
ips = append(ips, answer.IP.String())
|
||||
}
|
||||
if answer.TTL != 0 {
|
||||
ttl = int(answer.TTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ips, ttl
|
||||
}
|
||||
|
||||
// exchange choose a upstream dns server based on qname, communicate with it on the network.
|
||||
func (c *Client) exchange(qname string, reqBytes []byte, preferTCP bool) (
|
||||
server, network, dialerAddr string, respBytes []byte, err error) {
|
||||
|
||||
// use tcp to connect upstream server default
|
||||
network = "tcp"
|
||||
dialer := c.proxy.NextDialer(qname + ":0")
|
||||
|
||||
// if we are resolving a domain which uses a forwarder `REJECT`, then use `DIRECT` instead
|
||||
// so we can resolve it correctly.
|
||||
// TODO: dialer.Addr() == "REJECT", tricky
|
||||
if dialer.Addr() == "REJECT" {
|
||||
dialer = c.proxy.NextDialer("direct:0")
|
||||
}
|
||||
|
||||
// If client uses udp and no forwarders specified, use udp
|
||||
// TODO: dialer.Addr() == "DIRECT", tricky
|
||||
if !preferTCP && !c.config.AlwaysTCP && dialer.Addr() == "DIRECT" {
|
||||
network = "udp"
|
||||
}
|
||||
|
||||
ups := c.UpStream(qname)
|
||||
server = ups.Server()
|
||||
for range ups.Len() {
|
||||
var rc net.Conn
|
||||
rc, err = dialer.Dial(network, server)
|
||||
if err != nil {
|
||||
newServer := ups.SwitchIf(server)
|
||||
log.F("[dns] error in resolving %s, failed to connect to server %v via %s: %v, next server: %s",
|
||||
qname, server, dialer.Addr(), err, newServer)
|
||||
server = newServer
|
||||
continue
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// TODO: support timeout setting for different upstream server
|
||||
if c.config.Timeout > 0 {
|
||||
rc.SetDeadline(time.Now().Add(time.Duration(c.config.Timeout) * time.Second))
|
||||
}
|
||||
|
||||
switch network {
|
||||
case "tcp":
|
||||
respBytes, err = c.exchangeTCP(rc, reqBytes)
|
||||
case "udp":
|
||||
respBytes, err = c.exchangeUDP(rc, reqBytes)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
newServer := ups.SwitchIf(server)
|
||||
log.F("[dns] error in resolving %s, failed to exchange with server %v via %s: %v, next server: %s",
|
||||
qname, server, dialer.Addr(), err, newServer)
|
||||
|
||||
server = newServer
|
||||
}
|
||||
|
||||
// if all dns upstreams failed, then maybe the forwarder is not available.
|
||||
if err != nil {
|
||||
c.proxy.Record(dialer, false)
|
||||
}
|
||||
|
||||
return server, network, dialer.Addr(), respBytes, err
|
||||
}
|
||||
|
||||
// exchangeTCP exchange with server over tcp.
|
||||
func (c *Client) exchangeTCP(rc net.Conn, reqBytes []byte) ([]byte, error) {
|
||||
lenBuf := pool.GetBuffer(2)
|
||||
defer pool.PutBuffer(lenBuf)
|
||||
|
||||
binary.BigEndian.PutUint16(lenBuf, uint16(len(reqBytes)))
|
||||
if _, err := (&net.Buffers{lenBuf, reqBytes}).WriteTo(rc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var respLen uint16
|
||||
if err := binary.Read(rc, binary.BigEndian, &respLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respBytes := pool.GetBuffer(int(respLen))
|
||||
_, err := io.ReadFull(rc, respBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
// exchangeUDP exchange with server over udp.
|
||||
func (c *Client) exchangeUDP(rc net.Conn, reqBytes []byte) ([]byte, error) {
|
||||
if _, err := rc.Write(reqBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respBytes := pool.GetBuffer(UDPMaxLen)
|
||||
n, err := rc.Read(respBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return respBytes[:n], nil
|
||||
}
|
||||
|
||||
// SetServers sets upstream dns servers for the given domain.
|
||||
func (c *Client) SetServers(domain string, servers []string) {
|
||||
c.upStreamMap[strings.ToLower(domain)] = NewUPStream(servers)
|
||||
}
|
||||
|
||||
// UpStream returns upstream dns server for the given domain.
|
||||
func (c *Client) UpStream(domain string) *UPStream {
|
||||
domain = strings.ToLower(domain)
|
||||
for i := len(domain); i != -1; {
|
||||
i = strings.LastIndexByte(domain[:i], '.')
|
||||
if upstream, ok := c.upStreamMap[domain[i+1:]]; ok {
|
||||
return upstream
|
||||
}
|
||||
}
|
||||
return c.upStream
|
||||
}
|
||||
|
||||
// AddHandler adds a custom handler to handle the resolved result (A and AAAA).
|
||||
func (c *Client) AddHandler(h AnswerHandler) {
|
||||
c.handlers = append(c.handlers, h)
|
||||
}
|
||||
|
||||
// AddRecord adds custom record to dns cache, format:
|
||||
// www.example.com/1.2.3.4 or www.example.com/2606:2800:220:1:248:1893:25c8:1946
|
||||
func (c *Client) AddRecord(record string) error {
|
||||
domain, ip, found := strings.Cut(record, "/")
|
||||
if !found {
|
||||
return errors.New("wrong record format, must contain '/'")
|
||||
}
|
||||
m, err := MakeResponse(domain, ip, uint32(c.config.MaxTTL))
|
||||
if err != nil {
|
||||
log.F("[dns] add custom record error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
wb := pool.GetBytesBuffer()
|
||||
defer pool.PutBytesBuffer(wb)
|
||||
|
||||
_, err = m.MarshalTo(wb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.cache.Set(qKey(m.Question), valCopy(wb.Bytes()), 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeResponse makes a dns response message for the given domain and ip address.
|
||||
// Note: you should make sure ttl > 0.
|
||||
func MakeResponse(domain, ip string, ttl uint32) (*Message, error) {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var qtype, rdlen uint16 = QTypeA, net.IPv4len
|
||||
if addr.Is6() {
|
||||
qtype, rdlen = QTypeAAAA, net.IPv6len
|
||||
}
|
||||
|
||||
m := NewMessage(0, ResponseMsg)
|
||||
m.SetQuestion(NewQuestion(qtype, domain))
|
||||
rr := &RR{NAME: domain, TYPE: qtype, CLASS: ClassINET,
|
||||
TTL: ttl, RDLENGTH: rdlen, RDATA: addr.AsSlice()}
|
||||
m.AddAnswer(rr)
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func qKey(q *Question) string {
|
||||
return q.QNAME + "/" + strconv.FormatUint(uint64(q.QTYPE), 10)
|
||||
}
|
||||
|
||||
func valCopy(v []byte) (b []byte) {
|
||||
if v != nil {
|
||||
b = pool.GetBuffer(len(v))
|
||||
copy(b, v)
|
||||
}
|
||||
return
|
||||
}
|
527
dns/message.go
Normal file
527
dns/message.go
Normal file
@ -0,0 +1,527 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"math/rand/v2"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UDPMaxLen is the max size of udp dns request.
|
||||
// https://www.dnsflagday.net/2020/
|
||||
const UDPMaxLen = 1232
|
||||
|
||||
// HeaderLen is the length of dns msg header.
|
||||
const HeaderLen = 12
|
||||
|
||||
// MsgType is the dns Message type.
|
||||
type MsgType byte
|
||||
|
||||
// Message types.
|
||||
const (
|
||||
QueryMsg MsgType = 0
|
||||
ResponseMsg MsgType = 1
|
||||
)
|
||||
|
||||
// Query types.
|
||||
const (
|
||||
QTypeA uint16 = 1 //ipv4
|
||||
QTypeAAAA uint16 = 28 ///ipv6
|
||||
)
|
||||
|
||||
// ClassINET .
|
||||
const ClassINET uint16 = 1
|
||||
|
||||
// Message format:
|
||||
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1
|
||||
// All communications inside of the domain protocol are carried in a single
|
||||
// format called a message. The top level format of message is divided
|
||||
// into 5 sections (some of which are empty in certain cases) shown below:
|
||||
//
|
||||
// +---------------------+
|
||||
// | Header |
|
||||
// +---------------------+
|
||||
// | Question | the question for the name server
|
||||
// +---------------------+
|
||||
// | Answer | RRs answering the question
|
||||
// +---------------------+
|
||||
// | Authority | RRs pointing toward an authority
|
||||
// +---------------------+
|
||||
// | Additional | RRs holding additional information
|
||||
type Message struct {
|
||||
Header
|
||||
// most dns implementation only support 1 question
|
||||
Question *Question
|
||||
Answers []*RR
|
||||
Authority []*RR
|
||||
Additional []*RR
|
||||
|
||||
// used in UnmarshalMessage
|
||||
unMarshaled []byte
|
||||
}
|
||||
|
||||
// NewMessage returns a new message.
|
||||
func NewMessage(id uint16, msgType MsgType) *Message {
|
||||
if id == 0 {
|
||||
id = uint16(rand.Uint32())
|
||||
}
|
||||
|
||||
m := &Message{Header: Header{ID: id}}
|
||||
m.SetMsgType(msgType)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// SetQuestion sets a question to dns message.
|
||||
func (m *Message) SetQuestion(q *Question) error {
|
||||
m.Question = q
|
||||
m.Header.SetQdcount(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddAnswer adds an answer to dns message.
|
||||
func (m *Message) AddAnswer(rr *RR) error {
|
||||
m.Answers = append(m.Answers, rr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Marshal marshals message struct to []byte.
|
||||
func (m *Message) Marshal() ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
if _, err := m.MarshalTo(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// MarshalTo marshals message struct to []byte and write to w.
|
||||
func (m *Message) MarshalTo(w io.Writer) (n int, err error) {
|
||||
m.Header.SetQdcount(1)
|
||||
m.Header.SetAncount(len(m.Answers))
|
||||
|
||||
nn := 0
|
||||
nn, err = m.Header.MarshalTo(w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n += nn
|
||||
|
||||
nn, err = m.Question.MarshalTo(w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n += nn
|
||||
|
||||
for _, answer := range m.Answers {
|
||||
nn, err = answer.MarshalTo(w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n += nn
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalMessage unmarshals []bytes to Message.
|
||||
func UnmarshalMessage(b []byte) (*Message, error) {
|
||||
if len(b) < HeaderLen {
|
||||
return nil, errors.New("UnmarshalMessage: not enough data")
|
||||
}
|
||||
|
||||
m := &Message{unMarshaled: b}
|
||||
if err := UnmarshalHeader(b[:HeaderLen], &m.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := &Question{}
|
||||
qLen, err := m.UnmarshalQuestion(b[HeaderLen:], q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.SetQuestion(q)
|
||||
|
||||
// resp answers
|
||||
rrIdx := HeaderLen + qLen
|
||||
for range int(m.Header.ANCOUNT) {
|
||||
rr := &RR{}
|
||||
rrLen, err := m.UnmarshalRR(rrIdx, rr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.AddAnswer(rr)
|
||||
|
||||
rrIdx += rrLen
|
||||
}
|
||||
|
||||
m.Header.SetAncount(len(m.Answers))
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Header format:
|
||||
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.1
|
||||
// The header contains the following fields:
|
||||
//
|
||||
// 1 1 1 1 1 1
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | ID |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | QDCOUNT |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | ANCOUNT |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | NSCOUNT |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | ARCOUNT |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
type Header struct {
|
||||
ID uint16
|
||||
Bits uint16
|
||||
QDCOUNT uint16
|
||||
ANCOUNT uint16
|
||||
NSCOUNT uint16
|
||||
ARCOUNT uint16
|
||||
}
|
||||
|
||||
// SetMsgType sets the message type.
|
||||
func (h *Header) SetMsgType(qr MsgType) {
|
||||
h.Bits |= uint16(qr) << 15
|
||||
}
|
||||
|
||||
// SetTC sets the tc flag.
|
||||
func (h *Header) SetTC(tc int) {
|
||||
h.Bits |= uint16(tc) << 9
|
||||
}
|
||||
|
||||
// SetQdcount sets query count, most dns servers only support 1 query per request.
|
||||
func (h *Header) SetQdcount(qdcount int) {
|
||||
h.QDCOUNT = uint16(qdcount)
|
||||
}
|
||||
|
||||
// SetAncount sets answers count.
|
||||
func (h *Header) SetAncount(ancount int) {
|
||||
h.ANCOUNT = uint16(ancount)
|
||||
}
|
||||
|
||||
// Not used now, but keep it for future use.
|
||||
// func (h *Header) setFlag(QR uint16, Opcode uint16, AA uint16,
|
||||
// TC uint16, RD uint16, RA uint16, RCODE uint16) {
|
||||
// h.Bits = QR<<15 + Opcode<<11 + AA<<10 + TC<<9 + RD<<8 + RA<<7 + RCODE
|
||||
// }
|
||||
|
||||
// MarshalTo marshals header struct to []byte and write to w.
|
||||
func (h *Header) MarshalTo(w io.Writer) (int, error) {
|
||||
return HeaderLen, binary.Write(w, binary.BigEndian, h)
|
||||
}
|
||||
|
||||
// UnmarshalHeader unmarshals []bytes to Header.
|
||||
func UnmarshalHeader(b []byte, h *Header) error {
|
||||
if h == nil {
|
||||
return errors.New("unmarshal header must not be nil")
|
||||
}
|
||||
|
||||
if len(b) != HeaderLen {
|
||||
return errors.New("unmarshal header bytes has an unexpected size")
|
||||
}
|
||||
|
||||
h.ID = binary.BigEndian.Uint16(b[:2])
|
||||
h.Bits = binary.BigEndian.Uint16(b[2:4])
|
||||
h.QDCOUNT = binary.BigEndian.Uint16(b[4:6])
|
||||
h.ANCOUNT = binary.BigEndian.Uint16(b[6:8])
|
||||
h.NSCOUNT = binary.BigEndian.Uint16(b[8:10])
|
||||
h.ARCOUNT = binary.BigEndian.Uint16(b[10:])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Question format:
|
||||
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.2
|
||||
// The question section is used to carry the "question" in most queries,
|
||||
// i.e., the parameters that define what is being asked. The section
|
||||
// contains QDCOUNT (usually 1) entries, each of the following format:
|
||||
//
|
||||
// 1 1 1 1 1 1
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | |
|
||||
// / QNAME /
|
||||
// / /
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | QTYPE |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | QCLASS |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
type Question struct {
|
||||
QNAME string
|
||||
QTYPE uint16
|
||||
QCLASS uint16
|
||||
}
|
||||
|
||||
// NewQuestion returns a new dns question.
|
||||
func NewQuestion(qtype uint16, domain string) *Question {
|
||||
return &Question{
|
||||
QNAME: domain,
|
||||
QTYPE: qtype,
|
||||
QCLASS: ClassINET,
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalTo marshals Question struct to []byte and write to w.
|
||||
func (q *Question) MarshalTo(w io.Writer) (n int, err error) {
|
||||
n, err = MarshalDomainTo(w, q.QNAME)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = binary.Write(w, binary.BigEndian, q.QTYPE); err != nil {
|
||||
return
|
||||
}
|
||||
n += 2
|
||||
|
||||
if err = binary.Write(w, binary.BigEndian, q.QCLASS); err != nil {
|
||||
return
|
||||
}
|
||||
n += 2
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalQuestion unmarshals []bytes to Question.
|
||||
func (m *Message) UnmarshalQuestion(b []byte, q *Question) (n int, err error) {
|
||||
if q == nil {
|
||||
return 0, errors.New("unmarshal question must not be nil")
|
||||
}
|
||||
|
||||
if len(b) <= 5 {
|
||||
return 0, errors.New("UnmarshalQuestion: not enough data")
|
||||
}
|
||||
|
||||
sb := new(strings.Builder)
|
||||
sb.Grow(32)
|
||||
idx, err := m.UnmarshalDomainTo(sb, b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
q.QNAME = sb.String()
|
||||
q.QTYPE = binary.BigEndian.Uint16(b[idx : idx+2])
|
||||
q.QCLASS = binary.BigEndian.Uint16(b[idx+2 : idx+4])
|
||||
|
||||
return idx + 3 + 1, nil
|
||||
}
|
||||
|
||||
// RR format:
|
||||
// https://www.rfc-editor.org/rfc/rfc1035#section-3.2.1
|
||||
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.3
|
||||
// The answer, authority, and additional sections all share the same
|
||||
// format: a variable number of resource records, where the number of
|
||||
// records is specified in the corresponding count field in the header.
|
||||
// Each resource record has the following format:
|
||||
//
|
||||
// 1 1 1 1 1 1
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | |
|
||||
// / /
|
||||
// / NAME /
|
||||
// | |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | TYPE |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | CLASS |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | TTL |
|
||||
// | |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | RDLENGTH |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
|
||||
// / RDATA /
|
||||
// / /
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
type RR struct {
|
||||
NAME string
|
||||
TYPE uint16
|
||||
CLASS uint16
|
||||
TTL uint32
|
||||
RDLENGTH uint16
|
||||
RDATA []byte
|
||||
|
||||
IP netip.Addr
|
||||
}
|
||||
|
||||
// NewRR returns a new dns rr.
|
||||
func NewRR() *RR {
|
||||
return &RR{}
|
||||
}
|
||||
|
||||
// MarshalTo marshals RR struct to []byte and write to w.
|
||||
func (rr *RR) MarshalTo(w io.Writer) (n int, err error) {
|
||||
n, err = MarshalDomainTo(w, rr.NAME)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = binary.Write(w, binary.BigEndian, rr.TYPE); err != nil {
|
||||
return
|
||||
}
|
||||
n += 2
|
||||
|
||||
if err = binary.Write(w, binary.BigEndian, rr.CLASS); err != nil {
|
||||
return
|
||||
}
|
||||
n += 2
|
||||
|
||||
if err = binary.Write(w, binary.BigEndian, rr.TTL); err != nil {
|
||||
return
|
||||
}
|
||||
n += 4
|
||||
|
||||
err = binary.Write(w, binary.BigEndian, rr.RDLENGTH)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n += 2
|
||||
|
||||
if _, err = w.Write(rr.RDATA); err != nil {
|
||||
return
|
||||
}
|
||||
n += len(rr.RDATA)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalRR unmarshals []bytes to RR.
|
||||
func (m *Message) UnmarshalRR(start int, rr *RR) (n int, err error) {
|
||||
if rr == nil {
|
||||
return 0, errors.New("unmarshal rr must not be nil")
|
||||
}
|
||||
|
||||
p := m.unMarshaled[start:]
|
||||
|
||||
sb := new(strings.Builder)
|
||||
sb.Grow(32)
|
||||
|
||||
n, err = m.UnmarshalDomainTo(sb, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rr.NAME = sb.String()
|
||||
|
||||
if len(p) <= n+10 {
|
||||
return 0, errors.New("UnmarshalRR: not enough data")
|
||||
}
|
||||
|
||||
rr.TYPE = binary.BigEndian.Uint16(p[n:])
|
||||
rr.CLASS = binary.BigEndian.Uint16(p[n+2:])
|
||||
rr.TTL = binary.BigEndian.Uint32(p[n+4:])
|
||||
rr.RDLENGTH = binary.BigEndian.Uint16(p[n+8:])
|
||||
|
||||
if len(p) < n+10+int(rr.RDLENGTH) {
|
||||
return 0, errors.New("UnmarshalRR: not enough data for RDATA")
|
||||
}
|
||||
|
||||
rr.RDATA = p[n+10 : n+10+int(rr.RDLENGTH)]
|
||||
|
||||
if rr.TYPE == QTypeA {
|
||||
rr.IP = netip.AddrFrom4(*(*[4]byte)(rr.RDATA[:4]))
|
||||
} else if rr.TYPE == QTypeAAAA {
|
||||
rr.IP = netip.AddrFrom16(*(*[16]byte)(rr.RDATA[:16]))
|
||||
}
|
||||
|
||||
n = n + 10 + int(rr.RDLENGTH)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// MarshalDomainTo marshals domain string struct to []byte and write to w.
|
||||
func MarshalDomainTo(w io.Writer, domain string) (n int, err error) {
|
||||
nn := 0
|
||||
for _, seg := range strings.Split(domain, ".") {
|
||||
nn, err = w.Write([]byte{byte(len(seg))})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n += nn
|
||||
|
||||
nn, err = io.WriteString(w, seg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n += nn
|
||||
}
|
||||
|
||||
nn, err = w.Write([]byte{0x00})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n += nn
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalDomainTo gets domain from bytes to string builder.
|
||||
func (m *Message) UnmarshalDomainTo(sb *strings.Builder, b []byte) (int, error) {
|
||||
var idx, size int
|
||||
|
||||
for len(b[idx:]) != 0 {
|
||||
// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.4
|
||||
// "Message compression",
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
// | 1 1| OFFSET |
|
||||
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
if b[idx]&0xC0 == 0xC0 {
|
||||
if len(b[idx:]) < 2 {
|
||||
return 0, errors.New("UnmarshalDomainTo: not enough size for compressed domain")
|
||||
}
|
||||
|
||||
offset := binary.BigEndian.Uint16(b[idx : idx+2])
|
||||
if err := m.UnmarshalDomainPointTo(sb, int(offset&0x3FFF)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
idx += 2
|
||||
break
|
||||
}
|
||||
|
||||
size = int(b[idx])
|
||||
idx++
|
||||
|
||||
// root domain name
|
||||
if size == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if size > 63 {
|
||||
return 0, errors.New("UnmarshalDomainTo: label size larger than 63")
|
||||
}
|
||||
|
||||
if idx+size > len(b) {
|
||||
return 0, errors.New("UnmarshalDomainTo: label size larger than msg length")
|
||||
}
|
||||
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteByte('.')
|
||||
}
|
||||
sb.Write(b[idx : idx+size])
|
||||
|
||||
idx += size
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
// UnmarshalDomainPointTo gets domain from offset point to string builder.
|
||||
func (m *Message) UnmarshalDomainPointTo(sb *strings.Builder, offset int) error {
|
||||
if offset > len(m.unMarshaled) {
|
||||
return errors.New("UnmarshalDomainPointTo: offset larger than msg length")
|
||||
}
|
||||
_, err := m.UnmarshalDomainTo(sb, m.unMarshaled[offset:])
|
||||
return err
|
||||
}
|
151
dns/server.go
Normal file
151
dns/server.go
Normal file
@ -0,0 +1,151 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// conn timeout, in seconds.
|
||||
const timeout = 30
|
||||
|
||||
// Server is a dns server struct.
|
||||
type Server struct {
|
||||
addr string
|
||||
// Client is used to communicate with upstream dns servers
|
||||
*Client
|
||||
}
|
||||
|
||||
// NewServer returns a new dns server.
|
||||
func NewServer(addr string, p proxy.Proxy, config *Config) (*Server, error) {
|
||||
c, err := NewClient(p, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
addr: addr,
|
||||
Client: c,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Start starts the dns forwarding server.
|
||||
// We use WaitGroup here to ensure both udp and tcp serer are completly running,
|
||||
// so we can start any other services later, since they may rely on dns service.
|
||||
func (s *Server) Start() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go s.ListenAndServeTCP(&wg)
|
||||
go s.ListenAndServeUDP(&wg)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// ListenAndServeUDP listen and serves on udp port.
|
||||
func (s *Server) ListenAndServeUDP(wg *sync.WaitGroup) {
|
||||
pc, err := net.ListenPacket("udp", s.addr)
|
||||
wg.Done()
|
||||
if err != nil {
|
||||
log.F("[dns] failed to listen on %s, error: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
defer pc.Close()
|
||||
|
||||
log.F("[dns] listening UDP on %s", s.addr)
|
||||
|
||||
for {
|
||||
reqBytes := pool.GetBuffer(UDPMaxLen)
|
||||
n, caddr, err := pc.ReadFrom(reqBytes)
|
||||
if err != nil {
|
||||
log.F("[dns] local read error: %v", err)
|
||||
pool.PutBuffer(reqBytes)
|
||||
continue
|
||||
}
|
||||
go s.ServePacket(pc, caddr, reqBytes[:n])
|
||||
}
|
||||
}
|
||||
|
||||
// ServePacket serves dns packet conn.
|
||||
func (s *Server) ServePacket(pc net.PacketConn, caddr net.Addr, reqBytes []byte) {
|
||||
respBytes, err := s.Exchange(reqBytes, caddr.String(), false)
|
||||
defer func() {
|
||||
pool.PutBuffer(reqBytes)
|
||||
pool.PutBuffer(respBytes)
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
log.F("[dns] error in exchange for %s: %s", caddr, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = pc.WriteTo(respBytes, caddr)
|
||||
if err != nil {
|
||||
log.F("[dns] error in local write to %s: %s", caddr, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServeTCP listen and serves on tcp port.
|
||||
func (s *Server) ListenAndServeTCP(wg *sync.WaitGroup) {
|
||||
l, err := net.Listen("tcp", s.addr)
|
||||
wg.Done()
|
||||
if err != nil {
|
||||
log.F("[dns-tcp] error: %v", err)
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
log.F("[dns-tcp] listening TCP on %s", s.addr)
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
log.F("[dns-tcp] error: failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
go s.ServeTCP(c)
|
||||
}
|
||||
}
|
||||
|
||||
// ServeTCP serves a dns tcp connection.
|
||||
func (s *Server) ServeTCP(c net.Conn) {
|
||||
defer c.Close()
|
||||
|
||||
c.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
|
||||
|
||||
var reqLen uint16
|
||||
if err := binary.Read(c, binary.BigEndian, &reqLen); err != nil {
|
||||
log.F("[dns-tcp] failed to get request length: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
reqBytes := pool.GetBuffer(int(reqLen))
|
||||
defer pool.PutBuffer(reqBytes)
|
||||
|
||||
_, err := io.ReadFull(c, reqBytes)
|
||||
if err != nil {
|
||||
log.F("[dns-tcp] error in read reqBytes %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
respBytes, err := s.Exchange(reqBytes, c.RemoteAddr().String(), true)
|
||||
defer pool.PutBuffer(respBytes)
|
||||
if err != nil {
|
||||
log.F("[dns-tcp] error in exchange: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
lenBuf := pool.GetBuffer(2)
|
||||
defer pool.PutBuffer(lenBuf)
|
||||
|
||||
binary.BigEndian.PutUint16(lenBuf, uint16(len(respBytes)))
|
||||
if _, err := (&net.Buffers{lenBuf, respBytes}).WriteTo(c); err != nil {
|
||||
log.F("[dns-tcp] error in write respBytes: %s", err)
|
||||
}
|
||||
}
|
46
dns/upstream.go
Normal file
46
dns/upstream.go
Normal file
@ -0,0 +1,46 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// UPStream is a dns upstream.
|
||||
type UPStream struct {
|
||||
index uint32
|
||||
servers []string
|
||||
}
|
||||
|
||||
// NewUPStream returns a new UpStream.
|
||||
func NewUPStream(servers []string) *UPStream {
|
||||
// default port for dns upstream servers
|
||||
for i, server := range servers {
|
||||
if _, port, _ := net.SplitHostPort(server); port == "" {
|
||||
servers[i] = net.JoinHostPort(server, "53")
|
||||
}
|
||||
}
|
||||
return &UPStream{servers: servers}
|
||||
}
|
||||
|
||||
// Server returns a dns server.
|
||||
func (u *UPStream) Server() string {
|
||||
return u.servers[atomic.LoadUint32(&u.index)%uint32(len(u.servers))]
|
||||
}
|
||||
|
||||
// Switch switches to the next dns server.
|
||||
func (u *UPStream) Switch() string {
|
||||
return u.servers[atomic.AddUint32(&u.index, 1)%uint32(len(u.servers))]
|
||||
}
|
||||
|
||||
// SwitchIf switches to the next dns server if needed.
|
||||
func (u *UPStream) SwitchIf(server string) string {
|
||||
if u.Server() == server {
|
||||
return u.Switch()
|
||||
}
|
||||
return u.Server()
|
||||
}
|
||||
|
||||
// Len returns the number of dns servers.
|
||||
func (u *UPStream) Len() int {
|
||||
return len(u.servers)
|
||||
}
|
40
dnstun.go
40
dnstun.go
@ -1,40 +0,0 @@
|
||||
// https://tools.ietf.org/html/rfc1035
|
||||
|
||||
package main
|
||||
|
||||
// DNSTun struct
|
||||
type DNSTun struct {
|
||||
*Forwarder // as client
|
||||
sDialer Dialer // dialer for server
|
||||
|
||||
raddr string
|
||||
|
||||
udp *DNS
|
||||
tcp *TCPTun
|
||||
}
|
||||
|
||||
// NewDNSTun returns a dns tunnel forwarder.
|
||||
func NewDNSTun(addr, raddr string, sDialer Dialer) (*DNSTun, error) {
|
||||
s := &DNSTun{
|
||||
Forwarder: NewForwarder(addr, nil),
|
||||
sDialer: sDialer,
|
||||
|
||||
raddr: raddr,
|
||||
}
|
||||
|
||||
s.udp, _ = NewDNS(addr, raddr, sDialer)
|
||||
s.tcp, _ = NewTCPTun(addr, raddr, sDialer)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ListenAndServe .
|
||||
func (s *DNSTun) ListenAndServe() {
|
||||
if s.udp != nil {
|
||||
go s.udp.ListenAndServe()
|
||||
}
|
||||
|
||||
if s.tcp != nil {
|
||||
s.tcp.ListenAndServe()
|
||||
}
|
||||
}
|
27
feature.go
Normal file
27
feature.go
Normal file
@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// comment out the services you don't need to make the compiled binary smaller.
|
||||
// _ "github.com/nadoo/glider/service/xxx"
|
||||
|
||||
// comment out the protocols you don't need to make the compiled binary smaller.
|
||||
_ "github.com/nadoo/glider/proxy/http"
|
||||
_ "github.com/nadoo/glider/proxy/kcp"
|
||||
_ "github.com/nadoo/glider/proxy/mixed"
|
||||
_ "github.com/nadoo/glider/proxy/obfs"
|
||||
_ "github.com/nadoo/glider/proxy/pxyproto"
|
||||
_ "github.com/nadoo/glider/proxy/reject"
|
||||
_ "github.com/nadoo/glider/proxy/smux"
|
||||
_ "github.com/nadoo/glider/proxy/socks4"
|
||||
_ "github.com/nadoo/glider/proxy/socks5"
|
||||
_ "github.com/nadoo/glider/proxy/ss"
|
||||
_ "github.com/nadoo/glider/proxy/ssh"
|
||||
_ "github.com/nadoo/glider/proxy/ssr"
|
||||
_ "github.com/nadoo/glider/proxy/tcp"
|
||||
_ "github.com/nadoo/glider/proxy/tls"
|
||||
_ "github.com/nadoo/glider/proxy/trojan"
|
||||
_ "github.com/nadoo/glider/proxy/udp"
|
||||
_ "github.com/nadoo/glider/proxy/vless"
|
||||
_ "github.com/nadoo/glider/proxy/vmess"
|
||||
_ "github.com/nadoo/glider/proxy/ws"
|
||||
)
|
12
feature_linux.go
Normal file
12
feature_linux.go
Normal file
@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// comment out the services you don't need to make the compiled binary smaller.
|
||||
_ "github.com/nadoo/glider/service/dhcpd"
|
||||
|
||||
// comment out the protocols you don't need to make the compiled binary smaller.
|
||||
_ "github.com/nadoo/glider/proxy/redir"
|
||||
_ "github.com/nadoo/glider/proxy/tproxy"
|
||||
_ "github.com/nadoo/glider/proxy/unix"
|
||||
_ "github.com/nadoo/glider/proxy/vsock"
|
||||
)
|
31
forwarder.go
31
forwarder.go
@ -1,31 +0,0 @@
|
||||
package main
|
||||
|
||||
import "net"
|
||||
|
||||
// Forwarder struct
|
||||
type Forwarder struct {
|
||||
addr string
|
||||
cDialer Dialer
|
||||
}
|
||||
|
||||
// NewForwarder returns a base forwarder
|
||||
func NewForwarder(addr string, cDialer Dialer) *Forwarder {
|
||||
if cDialer == nil {
|
||||
cDialer = Direct
|
||||
}
|
||||
|
||||
return &Forwarder{addr: addr, cDialer: cDialer}
|
||||
}
|
||||
|
||||
// Addr returns forwarder's address
|
||||
func (p *Forwarder) Addr() string { return p.addr }
|
||||
|
||||
// Dial to remote addr via cDialer
|
||||
func (p *Forwarder) Dial(network, addr string) (net.Conn, error) {
|
||||
return p.cDialer.Dial(network, addr)
|
||||
}
|
||||
|
||||
// NextDialer returns the next cDialer
|
||||
func (p *Forwarder) NextDialer(dstAddr string) Dialer {
|
||||
return p.cDialer
|
||||
}
|
29
go.mod
Normal file
29
go.mod
Normal file
@ -0,0 +1,29 @@
|
||||
module github.com/nadoo/glider
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da
|
||||
github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d
|
||||
github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb
|
||||
github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
|
||||
github.com/nadoo/conflag v0.3.1
|
||||
github.com/nadoo/ipset v0.5.0
|
||||
github.com/xtaci/kcp-go/v5 v5.6.18
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/sys v0.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/klauspost/reedsolomon v1.12.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/templexxx/cpu v0.1.1 // indirect
|
||||
github.com/templexxx/xorsimd v0.4.3 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
)
|
129
go.sum
Normal file
129
go.sum
Normal file
@ -0,0 +1,129 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d h1:CPqTNIigGweVPT4CYb+OO2E6XyRKFOmvTHwWRLgCAlE=
|
||||
github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d/go.mod h1:QX5ZVULjAfZJux/W62Y91HvCh9hyW6enAwcrrv/sLj0=
|
||||
github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb h1:zXpN5126w/mhECTkqazBkrOJIMatbPP71aSIDR5UuW4=
|
||||
github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb/go.mod h1:F7WkpqJj9t98ePxB/WJGQTIDeOVPuSJ3qdn6JUjg170=
|
||||
github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152 h1:ED31mPIxDJnrLt9W9dH5xgd/6KjzEACKHBVGQ33czc0=
|
||||
github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152/go.mod h1:I9fhc/EvSg88cDxmfQ47v35Ssz9rlFunL/KY0A1JAYI=
|
||||
github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 h1:fBHFH+Y/GPGFGo7LIrErQc3p2MeAhoIQNgaxPWYsSxk=
|
||||
github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:ucvhdsUCE3TH0LoLRb6ShHiJl8e39dGlx6A4g/ujlow=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA=
|
||||
github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU=
|
||||
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
|
||||
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/nadoo/conflag v0.3.1 h1:4pHkLIz8PUsfg6ajNYRRSY3bt6m2LPsu6KOzn5uIXQw=
|
||||
github.com/nadoo/conflag v0.3.1/go.mod h1:dzFfDUpXdr2uS2oV+udpy5N2vfNOu/bFzjhX1WI52co=
|
||||
github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc=
|
||||
github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
|
||||
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
||||
github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
|
||||
github.com/templexxx/xorsimd v0.4.3/go.mod h1:oZQcD6RFDisW2Am58dSAGwwL6rHjbzrlu25VDqfWkQg=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/xtaci/kcp-go/v5 v5.6.18 h1:7oV4mc272pcnn39/13BB11Bx7hJM4ogMIEokJYVWn4g=
|
||||
github.com/xtaci/kcp-go/v5 v5.6.18/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
|
||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
|
||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
271
http.go
271
http.go
@ -1,271 +0,0 @@
|
||||
// http proxy
|
||||
// NOTE: never keep-alive so the implementation can be much easier.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTP struct
|
||||
type HTTP struct {
|
||||
*Forwarder // as client
|
||||
sDialer Dialer // dialer for server
|
||||
|
||||
user string
|
||||
password string
|
||||
}
|
||||
|
||||
// NewHTTP returns a http proxy.
|
||||
func NewHTTP(addr, user, pass string, cDialer Dialer, sDialer Dialer) (*HTTP, error) {
|
||||
s := &HTTP{
|
||||
Forwarder: NewForwarder(addr, cDialer),
|
||||
sDialer: sDialer,
|
||||
user: user,
|
||||
password: pass,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ListenAndServe .
|
||||
func (s *HTTP) ListenAndServe() {
|
||||
l, err := net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
logf("failed to listen on %s: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
logf("listening TCP on %s", s.addr)
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
logf("failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go s.Serve(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve .
|
||||
func (s *HTTP) Serve(c net.Conn) {
|
||||
defer c.Close()
|
||||
|
||||
if c, ok := c.(*net.TCPConn); ok {
|
||||
c.SetKeepAlive(true)
|
||||
}
|
||||
|
||||
reqR := bufio.NewReader(c)
|
||||
reqTP := textproto.NewReader(reqR)
|
||||
method, requestURI, proto, ok := parseFirstLine(reqTP)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if method == "CONNECT" {
|
||||
s.servHTTPS(method, requestURI, proto, c)
|
||||
return
|
||||
}
|
||||
|
||||
reqHeader, err := reqTP.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
logf("read header error:%s", err)
|
||||
return
|
||||
}
|
||||
cleanHeaders(reqHeader)
|
||||
// tell the remote server not to keep alive
|
||||
reqHeader.Set("Connection", "close")
|
||||
|
||||
url, err := url.ParseRequestURI(requestURI)
|
||||
if err != nil {
|
||||
logf("parse request url error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
var tgt = url.Host
|
||||
if !strings.Contains(url.Host, ":") {
|
||||
tgt += ":80"
|
||||
}
|
||||
|
||||
rc, err := s.sDialer.Dial("tcp", tgt)
|
||||
if err != nil {
|
||||
fmt.Fprintf(c, "%s 502 ERROR\r\n\r\n", proto)
|
||||
logf("failed to dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// GET http://example.com/a/index.htm HTTP/1.1 -->
|
||||
// GET /a/index.htm HTTP/1.1
|
||||
url.Scheme = ""
|
||||
url.Host = ""
|
||||
uri := url.String()
|
||||
|
||||
var reqBuf bytes.Buffer
|
||||
writeFirstLine(method, uri, proto, &reqBuf)
|
||||
writeHeaders(reqHeader, &reqBuf)
|
||||
|
||||
// send request to remote server
|
||||
rc.Write(reqBuf.Bytes())
|
||||
|
||||
// copy the left request bytes to remote server. eg. length specificed or chunked body.
|
||||
go func() {
|
||||
io.Copy(rc, reqR)
|
||||
rc.SetDeadline(time.Now())
|
||||
c.SetDeadline(time.Now())
|
||||
}()
|
||||
|
||||
respR := bufio.NewReader(rc)
|
||||
respTP := textproto.NewReader(respR)
|
||||
proto, code, status, ok := parseFirstLine(respTP)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
respHeader, err := respTP.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
logf("read header error:%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
respHeader.Set("Proxy-Connection", "close")
|
||||
respHeader.Set("Connection", "close")
|
||||
|
||||
var respBuf bytes.Buffer
|
||||
writeFirstLine(proto, code, status, &respBuf)
|
||||
writeHeaders(respHeader, &respBuf)
|
||||
|
||||
logf("proxy-http %s <-> %s", c.RemoteAddr(), tgt)
|
||||
c.Write(respBuf.Bytes())
|
||||
|
||||
io.Copy(c, respR)
|
||||
|
||||
}
|
||||
|
||||
func (s *HTTP) servHTTPS(method, requestURI, proto string, c net.Conn) {
|
||||
rc, err := s.sDialer.Dial("tcp", requestURI)
|
||||
if err != nil {
|
||||
c.Write([]byte(proto))
|
||||
c.Write([]byte(" 502 ERROR\r\n\r\n"))
|
||||
logf("failed to dial: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n"))
|
||||
|
||||
logf("proxy-https %s <-> %s", c.RemoteAddr(), requestURI)
|
||||
|
||||
_, _, err = relay(c, rc)
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
return // ignore i/o timeout
|
||||
}
|
||||
logf("relay error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the network net via the proxy.
|
||||
func (s *HTTP) Dial(network, addr string) (net.Conn, error) {
|
||||
rc, err := s.cDialer.Dial("tcp", s.addr)
|
||||
if err != nil {
|
||||
logf("dial to %s error: %s", s.addr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c, ok := rc.(*net.TCPConn); ok {
|
||||
c.SetKeepAlive(true)
|
||||
}
|
||||
|
||||
rc.Write([]byte("CONNECT " + addr + " HTTP/1.0\r\n"))
|
||||
rc.Write([]byte("Proxy-Connection: close\r\n"))
|
||||
|
||||
if s.user != "" && s.password != "" {
|
||||
auth := s.user + ":" + s.password
|
||||
rc.Write([]byte("Proxy-Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + "\r\n"))
|
||||
}
|
||||
|
||||
//header ended
|
||||
rc.Write([]byte("\r\n"))
|
||||
|
||||
respR := bufio.NewReader(rc)
|
||||
respTP := textproto.NewReader(respR)
|
||||
_, code, _, ok := parseFirstLine(respTP)
|
||||
if ok && code == "200" {
|
||||
return rc, err
|
||||
} else if code == "407" {
|
||||
logf("proxy-http: authencation needed by proxy %s", s.addr)
|
||||
} else if code == "405" {
|
||||
logf("proxy-http: 'CONNECT' method not allowed by proxy %s", s.addr)
|
||||
}
|
||||
|
||||
return nil, errors.New("cound not connect remote address: " + addr + ". error code: " + code)
|
||||
}
|
||||
|
||||
// parseFirstLine parses "GET /foo HTTP/1.1" OR "HTTP/1.1 200 OK" into its three parts.
|
||||
func parseFirstLine(tp *textproto.Reader) (r1, r2, r3 string, ok bool) {
|
||||
line, err := tp.ReadLine()
|
||||
// logf("first line: %s", line)
|
||||
if err != nil {
|
||||
logf("read request line error:%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
s1 := strings.Index(line, " ")
|
||||
s2 := strings.Index(line[s1+1:], " ")
|
||||
if s1 < 0 || s2 < 0 {
|
||||
return
|
||||
}
|
||||
s2 += s1 + 1
|
||||
return line[:s1], line[s1+1 : s2], line[s2+1:], true
|
||||
}
|
||||
|
||||
func cleanHeaders(header textproto.MIMEHeader) {
|
||||
header.Del("Proxy-Connection")
|
||||
header.Del("Connection")
|
||||
header.Del("Keep-Alive")
|
||||
header.Del("Proxy-Authenticate")
|
||||
header.Del("Proxy-Authorization")
|
||||
header.Del("TE")
|
||||
header.Del("Trailers")
|
||||
header.Del("Transfer-Encoding")
|
||||
header.Del("Upgrade")
|
||||
}
|
||||
|
||||
func writeFirstLine(s1, s2, s3 string, buf *bytes.Buffer) {
|
||||
buf.Write([]byte(s1))
|
||||
buf.Write([]byte(" "))
|
||||
buf.Write([]byte(s2))
|
||||
buf.Write([]byte(" "))
|
||||
buf.Write([]byte(s3))
|
||||
buf.Write([]byte("\r\n"))
|
||||
}
|
||||
|
||||
func writeHeaders(header textproto.MIMEHeader, buf *bytes.Buffer) {
|
||||
for key, values := range header {
|
||||
buf.Write([]byte(key))
|
||||
buf.Write([]byte(": "))
|
||||
for k, v := range values {
|
||||
buf.Write([]byte(v))
|
||||
if k > 0 {
|
||||
buf.Write([]byte(" "))
|
||||
}
|
||||
}
|
||||
buf.Write([]byte("\r\n"))
|
||||
}
|
||||
|
||||
//header ended
|
||||
buf.Write([]byte("\r\n"))
|
||||
}
|
78
ipset/ipset_linux.go
Normal file
78
ipset/ipset_linux.go
Normal file
@ -0,0 +1,78 @@
|
||||
package ipset
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/nadoo/ipset"
|
||||
|
||||
"github.com/nadoo/glider/rule"
|
||||
)
|
||||
|
||||
// Manager struct.
|
||||
type Manager struct {
|
||||
domainSet sync.Map
|
||||
}
|
||||
|
||||
// NewManager returns a Manager
|
||||
func NewManager(rules []*rule.Config) (*Manager, error) {
|
||||
if err := ipset.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &Manager{}
|
||||
sets := make(map[string]struct{})
|
||||
|
||||
for _, r := range rules {
|
||||
if r.IPSet == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := sets[r.IPSet]; !ok {
|
||||
sets[r.IPSet] = struct{}{}
|
||||
ipset.Create(r.IPSet)
|
||||
ipset.Flush(r.IPSet)
|
||||
ipset.Create(r.IPSet+"6", ipset.OptIPv6())
|
||||
ipset.Flush(r.IPSet + "6")
|
||||
}
|
||||
|
||||
for _, domain := range r.Domain {
|
||||
m.domainSet.Store(domain, r.IPSet)
|
||||
}
|
||||
for _, ip := range r.IP {
|
||||
addToSet(r.IPSet, ip)
|
||||
}
|
||||
for _, cidr := range r.CIDR {
|
||||
addToSet(r.IPSet, cidr)
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// AddDomainIP implements the dns AnswerHandler function, used to update ipset according to domainSet rule.
|
||||
func (m *Manager) AddDomainIP(domain string, ip netip.Addr) error {
|
||||
domain = strings.ToLower(domain)
|
||||
for i := len(domain); i != -1; {
|
||||
i = strings.LastIndexByte(domain[:i], '.')
|
||||
if setName, ok := m.domainSet.Load(domain[i+1:]); ok {
|
||||
addAddrToSet(setName.(string), ip)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addToSet(s, item string) error {
|
||||
if strings.IndexByte(item, '.') == -1 {
|
||||
return ipset.Add(s+"6", item)
|
||||
}
|
||||
return ipset.Add(s, item)
|
||||
}
|
||||
|
||||
func addAddrToSet(s string, ip netip.Addr) error {
|
||||
if ip.Is4() {
|
||||
return ipset.AddAddr(s, ip)
|
||||
}
|
||||
return ipset.AddAddr(s+"6", ip)
|
||||
}
|
23
ipset/ipset_other.go
Normal file
23
ipset/ipset_other.go
Normal file
@ -0,0 +1,23 @@
|
||||
//go:build !linux
|
||||
|
||||
package ipset
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"github.com/nadoo/glider/rule"
|
||||
)
|
||||
|
||||
// Manager struct
|
||||
type Manager struct{}
|
||||
|
||||
// NewManager returns a Manager
|
||||
func NewManager(rules []*rule.Config) (*Manager, error) {
|
||||
return nil, errors.New("ipset not supported on this os")
|
||||
}
|
||||
|
||||
// AddDomainIP implements the DNSAnswerHandler function
|
||||
func (m *Manager) AddDomainIP(domain string, ip netip.Addr) error {
|
||||
return errors.New("ipset not supported on this os")
|
||||
}
|
472
ipset_linux.go
472
ipset_linux.go
@ -1,472 +0,0 @@
|
||||
// Apache License 2.0
|
||||
// @mdlayher https://github.com/mdlayher/netlink
|
||||
// Ref: https://github.com/vishvananda/netlink/blob/master/nl/nl_linux.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// netfilter netlink message types
|
||||
// https://github.com/torvalds/linux/blob/9e66317d3c92ddaab330c125dfe9d06eee268aff/include/uapi/linux/netfilter/nfnetlink.h#L56
|
||||
const NFNL_SUBSYS_IPSET = 6
|
||||
|
||||
// http://git.netfilter.org/ipset/tree/include/libipset/linux_ip_set.h
|
||||
/* The protocol version */
|
||||
const IPSET_PROTOCOL = 6
|
||||
|
||||
/* The max length of strings including NUL: set and type identifiers */
|
||||
const IPSET_MAXNAMELEN = 32
|
||||
|
||||
/* Message types and commands */
|
||||
const IPSET_CMD_CREATE = 2
|
||||
const IPSET_CMD_FLUSH = 4
|
||||
const IPSET_CMD_ADD = 9
|
||||
const IPSET_CMD_DEL = 10
|
||||
|
||||
/* Attributes at command level */
|
||||
const IPSET_ATTR_PROTOCOL = 1 /* 1: Protocol version */
|
||||
const IPSET_ATTR_SETNAME = 2 /* 2: Name of the set */
|
||||
const IPSET_ATTR_TYPENAME = 3 /* 3: Typename */
|
||||
const IPSET_ATTR_REVISION = 4 /* 4: Settype revision */
|
||||
const IPSET_ATTR_FAMILY = 5 /* 5: Settype family */
|
||||
const IPSET_ATTR_DATA = 7 /* 7: Nested attributes */
|
||||
|
||||
/* CADT specific attributes */
|
||||
const IPSET_ATTR_IP = 1
|
||||
const IPSET_ATTR_CIDR = 3
|
||||
|
||||
/* IP specific attributes */
|
||||
const IPSET_ATTR_IPADDR_IPV4 = 1
|
||||
const IPSET_ATTR_IPADDR_IPV6 = 2
|
||||
|
||||
const NLA_F_NESTED = (1 << 15)
|
||||
const NLA_F_NET_BYTEORDER = (1 << 14)
|
||||
|
||||
var nextSeqNr uint32
|
||||
var nativeEndian binary.ByteOrder
|
||||
|
||||
// IPSetManager struct
|
||||
type IPSetManager struct {
|
||||
fd int
|
||||
lsa syscall.SockaddrNetlink
|
||||
|
||||
mainSet string
|
||||
domainSet sync.Map
|
||||
}
|
||||
|
||||
// NewIPSetManager returns a IPSetManager
|
||||
func NewIPSetManager(mainSet string, rules []*RuleConf) (*IPSetManager, error) {
|
||||
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_NETFILTER)
|
||||
if err != nil {
|
||||
logf("%s", err)
|
||||
return nil, err
|
||||
}
|
||||
// defer syscall.Close(fd)
|
||||
|
||||
lsa := syscall.SockaddrNetlink{
|
||||
Family: syscall.AF_NETLINK,
|
||||
}
|
||||
|
||||
if err = syscall.Bind(fd, &lsa); err != nil {
|
||||
logf("%s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &IPSetManager{fd: fd, lsa: lsa, mainSet: mainSet}
|
||||
|
||||
CreateSet(fd, lsa, mainSet)
|
||||
|
||||
for _, r := range rules {
|
||||
|
||||
set := r.IPSet
|
||||
|
||||
if set != "" && set != m.mainSet {
|
||||
CreateSet(fd, lsa, set)
|
||||
} else {
|
||||
set = m.mainSet
|
||||
}
|
||||
|
||||
for _, domain := range r.Domain {
|
||||
m.domainSet.Store(domain, set)
|
||||
}
|
||||
|
||||
for _, ip := range r.IP {
|
||||
AddToSet(fd, lsa, mainSet, ip)
|
||||
AddToSet(fd, lsa, r.IPSet, ip)
|
||||
}
|
||||
|
||||
for _, cidr := range r.CIDR {
|
||||
AddToSet(fd, lsa, mainSet, cidr)
|
||||
AddToSet(fd, lsa, r.IPSet, cidr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// AddDomainIP implements the DNSAnswerHandler function, used to update ipset according to domainSet rule
|
||||
func (m *IPSetManager) AddDomainIP(domain, ip string) error {
|
||||
|
||||
if ip != "" {
|
||||
domainParts := strings.Split(domain, ".")
|
||||
length := len(domainParts)
|
||||
for i := length - 2; i >= 0; i-- {
|
||||
domain := strings.Join(domainParts[i:length], ".")
|
||||
|
||||
// find in domainMap
|
||||
if ipset, ok := m.domainSet.Load(domain); ok {
|
||||
AddToSet(m.fd, m.lsa, m.mainSet, ip)
|
||||
AddToSet(m.fd, m.lsa, ipset.(string), ip)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateSet(fd int, lsa syscall.SockaddrNetlink, setName string) {
|
||||
if setName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if len(setName) > IPSET_MAXNAMELEN {
|
||||
log.Fatal("ipset name too long")
|
||||
}
|
||||
|
||||
logf("ipset: create %s hash:net", setName)
|
||||
|
||||
req := NewNetlinkRequest(IPSET_CMD_CREATE|(NFNL_SUBSYS_IPSET<<8), syscall.NLM_F_REQUEST)
|
||||
|
||||
// TODO: support AF_INET6
|
||||
nfgenMsg := NewNfGenMsg(syscall.AF_INET, 0, 0)
|
||||
req.AddData(nfgenMsg)
|
||||
|
||||
attrProto := NewRtAttr(IPSET_ATTR_PROTOCOL, Uint8Attr(IPSET_PROTOCOL))
|
||||
req.AddData(attrProto)
|
||||
|
||||
attrSiteName := NewRtAttr(IPSET_ATTR_SETNAME, ZeroTerminated(setName))
|
||||
req.AddData(attrSiteName)
|
||||
|
||||
attrSiteType := NewRtAttr(IPSET_ATTR_TYPENAME, ZeroTerminated("hash:net"))
|
||||
req.AddData(attrSiteType)
|
||||
|
||||
attrRev := NewRtAttr(IPSET_ATTR_REVISION, Uint8Attr(1))
|
||||
req.AddData(attrRev)
|
||||
|
||||
attrFamily := NewRtAttr(IPSET_ATTR_FAMILY, Uint8Attr(2))
|
||||
req.AddData(attrFamily)
|
||||
|
||||
attrData := NewRtAttr(IPSET_ATTR_DATA|NLA_F_NESTED, nil)
|
||||
req.AddData(attrData)
|
||||
|
||||
err := syscall.Sendto(fd, req.Serialize(), 0, &lsa)
|
||||
if err != nil {
|
||||
logf("%s", err)
|
||||
}
|
||||
|
||||
FlushSet(fd, lsa, setName)
|
||||
}
|
||||
|
||||
func FlushSet(fd int, lsa syscall.SockaddrNetlink, setName string) {
|
||||
logf("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 {
|
||||
logf("%s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func AddToSet(fd int, lsa syscall.SockaddrNetlink, setName, entry string) {
|
||||
if setName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if len(setName) > IPSET_MAXNAMELEN {
|
||||
logf("ipset name too long")
|
||||
}
|
||||
|
||||
logf("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 {
|
||||
logf("ipset: parse %s error", entry)
|
||||
return
|
||||
}
|
||||
|
||||
req := NewNetlinkRequest(IPSET_CMD_ADD|(NFNL_SUBSYS_IPSET<<8), syscall.NLM_F_REQUEST)
|
||||
|
||||
// TODO: support AF_INET6
|
||||
nfgenMsg := NewNfGenMsg(syscall.AF_INET, 0, 0)
|
||||
req.AddData(nfgenMsg)
|
||||
|
||||
attrProto := NewRtAttr(IPSET_ATTR_PROTOCOL, Uint8Attr(IPSET_PROTOCOL))
|
||||
req.AddData(attrProto)
|
||||
|
||||
attrSiteName := NewRtAttr(IPSET_ATTR_SETNAME, ZeroTerminated(setName))
|
||||
req.AddData(attrSiteName)
|
||||
|
||||
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 {
|
||||
logf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
type NetlinkRequestData interface {
|
||||
Len() int
|
||||
Serialize() []byte
|
||||
}
|
||||
|
||||
type NfGenMsg struct {
|
||||
nfgenFamily uint8
|
||||
version uint8
|
||||
resID uint16
|
||||
}
|
||||
|
||||
func NewNfGenMsg(nfgenFamily, version, resID int) *NfGenMsg {
|
||||
return &NfGenMsg{
|
||||
nfgenFamily: uint8(nfgenFamily),
|
||||
version: uint8(version),
|
||||
resID: uint16(resID),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *NfGenMsg) Len() int {
|
||||
return rtaAlignOf(4)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Extend RtAttr to handle data and children
|
||||
type RtAttr struct {
|
||||
syscall.RtAttr
|
||||
Data []byte
|
||||
children []NetlinkRequestData
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type NetlinkRequest struct {
|
||||
syscall.NlMsghdr
|
||||
Data []NetlinkRequestData
|
||||
RawData []byte
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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...)
|
||||
}
|
||||
}
|
||||
|
||||
func Uint8Attr(v uint8) []byte {
|
||||
return []byte{byte(v)}
|
||||
}
|
||||
|
||||
func Uint16Attr(v uint16) []byte {
|
||||
native := NativeEndian()
|
||||
bytes := make([]byte, 2)
|
||||
native.PutUint16(bytes, v)
|
||||
return bytes
|
||||
}
|
||||
|
||||
func Uint32Attr(v uint32) []byte {
|
||||
native := NativeEndian()
|
||||
bytes := make([]byte, 4)
|
||||
native.PutUint32(bytes, v)
|
||||
return bytes
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func NonZeroTerminated(s string) []byte {
|
||||
bytes := make([]byte, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
bytes[i] = s[i]
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
func BytesToString(b []byte) string {
|
||||
n := bytes.Index(b, []byte{0})
|
||||
return string(b[:n])
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
// +build !linux
|
||||
|
||||
package main
|
||||
|
||||
import "errors"
|
||||
|
||||
// IPSetManager struct
|
||||
type IPSetManager struct{}
|
||||
|
||||
// NewIPSetManager returns a IPSetManager
|
||||
func NewIPSetManager(mainSet string, rules []*RuleConf) (*IPSetManager, error) {
|
||||
return nil, errors.New("ipset not supported on this os")
|
||||
}
|
||||
|
||||
// AddDomainIP implements the DNSAnswerHandler function
|
||||
func (m *IPSetManager) AddDomainIP(domain, ip string) error {
|
||||
return errors.New("ipset not supported on this os")
|
||||
}
|
16
log.go
16
log.go
@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import "log"
|
||||
|
||||
// LogFunc defines a simple log function
|
||||
type LogFunc func(f string, v ...interface{})
|
||||
|
||||
var logf LogFunc
|
||||
|
||||
func init() {
|
||||
logf = func(f string, v ...interface{}) {
|
||||
if conf.Verbose {
|
||||
log.Printf(f, v...)
|
||||
}
|
||||
}
|
||||
}
|
111
main.go
111
main.go
@ -1,75 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// VERSION .
|
||||
const VERSION = "0.4.3"
|
||||
|
||||
func dialerFromConf() Dialer {
|
||||
// global forwarders in xx.conf
|
||||
var forwarders []Dialer
|
||||
for _, chain := range conf.Forward {
|
||||
var forward Dialer
|
||||
var err error
|
||||
for _, url := range strings.Split(chain, ",") {
|
||||
forward, err = DialerFromURL(url, forward)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
forwarders = append(forwarders, forward)
|
||||
}
|
||||
|
||||
return NewStrategyDialer(conf.Strategy, forwarders, conf.CheckWebSite, conf.CheckDuration)
|
||||
}
|
||||
var (
|
||||
version = "0.17.0"
|
||||
config = parseConfig()
|
||||
)
|
||||
|
||||
func main() {
|
||||
// global rule proxy
|
||||
pxy := rule.NewProxy(config.Forwards, &config.Strategy, config.rules)
|
||||
|
||||
confInit()
|
||||
sDialer := NewRuleDialer(conf.rules, dialerFromConf())
|
||||
// ipset manager
|
||||
ipsetM, _ := ipset.NewManager(config.rules)
|
||||
|
||||
for _, listen := range conf.Listen {
|
||||
local, err := ServerFromURL(listen, sDialer)
|
||||
// check and setup dns server
|
||||
if config.DNS != "" {
|
||||
d, err := dns.NewServer(config.DNS, pxy, &config.DNSConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go local.ListenAndServe()
|
||||
}
|
||||
|
||||
ipsetM, err := NewIPSetManager(conf.IPSet, conf.rules)
|
||||
if err != nil {
|
||||
logf("create ipset manager error: %s", err)
|
||||
}
|
||||
|
||||
if conf.DNS != "" {
|
||||
dns, err := NewDNS(conf.DNS, conf.DNSServer[0], sDialer)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// rule
|
||||
for _, frwder := range conf.rules {
|
||||
for _, domain := range frwder.Domain {
|
||||
if len(frwder.DNSServer) > 0 {
|
||||
dns.SetServer(domain, frwder.DNSServer[0])
|
||||
// rules
|
||||
for _, r := range config.rules {
|
||||
if len(r.DNSServers) > 0 {
|
||||
for _, domain := range r.Domain {
|
||||
d.SetServers(domain, r.DNSServers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add a handler to update proxy rules when a domain resolved
|
||||
dns.AddAnswerHandler(sDialer.AddDomainIP)
|
||||
d.AddHandler(pxy.AddDomainIP)
|
||||
if ipsetM != nil {
|
||||
dns.AddAnswerHandler(ipsetM.AddDomainIP)
|
||||
d.AddHandler(ipsetM.AddDomainIP)
|
||||
}
|
||||
|
||||
go dns.ListenAndServe()
|
||||
d.Start()
|
||||
|
||||
// custom resolver
|
||||
net.DefaultResolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{Timeout: time.Second * 3}
|
||||
return d.DialContext(ctx, "udp", config.DNS)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range config.rules {
|
||||
r.IP, r.CIDR, r.Domain = nil, nil, nil
|
||||
}
|
||||
|
||||
// enable checkers
|
||||
pxy.Check()
|
||||
|
||||
// run proxy servers
|
||||
for _, listen := range config.Listens {
|
||||
local, err := proxy.ServerFromURL(listen, pxy)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go local.ListenAndServe()
|
||||
}
|
||||
|
||||
// run services
|
||||
for _, s := range config.Services {
|
||||
service, err := service.New(s)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go service.Run()
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
|
102
mixed.go
102
mixed.go
@ -1,102 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
)
|
||||
|
||||
// https://www.ietf.org/rfc/rfc2616.txt, http methods must be uppercase.
|
||||
var httpMethods = [...][]byte{
|
||||
[]byte("GET"),
|
||||
[]byte("POST"),
|
||||
[]byte("PUT"),
|
||||
[]byte("DELETE"),
|
||||
[]byte("CONNECT"),
|
||||
[]byte("HEAD"),
|
||||
[]byte("OPTIONS"),
|
||||
[]byte("TRACE"),
|
||||
}
|
||||
|
||||
// MixedProxy struct
|
||||
type MixedProxy struct {
|
||||
sDialer Dialer
|
||||
|
||||
addr string
|
||||
http *HTTP
|
||||
socks5 *SOCKS5
|
||||
}
|
||||
|
||||
// NewMixedProxy returns a mixed proxy.
|
||||
func NewMixedProxy(addr, user, pass string, sDialer Dialer) (*MixedProxy, error) {
|
||||
p := &MixedProxy{
|
||||
sDialer: sDialer,
|
||||
addr: addr,
|
||||
}
|
||||
|
||||
p.http, _ = NewHTTP(addr, user, pass, nil, sDialer)
|
||||
p.socks5, _ = NewSOCKS5(addr, user, pass, nil, sDialer)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// ListenAndServe .
|
||||
func (p *MixedProxy) ListenAndServe() {
|
||||
l, err := net.Listen("tcp", p.addr)
|
||||
if err != nil {
|
||||
logf("failed to listen on %s: %v", p.addr, err)
|
||||
return
|
||||
}
|
||||
|
||||
logf("listening TCP on %s", p.addr)
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
logf("failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go p.Serve(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve .
|
||||
func (p *MixedProxy) Serve(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
if c, ok := conn.(*net.TCPConn); ok {
|
||||
c.SetKeepAlive(true)
|
||||
}
|
||||
|
||||
c := newConn(conn)
|
||||
|
||||
if p.socks5 != nil {
|
||||
head, err := c.Peek(1)
|
||||
if err != nil {
|
||||
logf("peek error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// check socks5, client send socksversion: 5 as the first byte
|
||||
if head[0] == socks5Version {
|
||||
p.socks5.Serve(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if p.http != nil {
|
||||
head, err := c.Peek(8)
|
||||
if err != nil {
|
||||
logf("peek error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, method := range httpMethods {
|
||||
if bytes.HasPrefix(head, method) {
|
||||
p.http.Serve(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
41
pkg/log/log.go
Normal file
41
pkg/log/log.go
Normal file
@ -0,0 +1,41 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
)
|
||||
|
||||
var enable = false
|
||||
|
||||
// Set sets the logger's verbose mode and output flags.
|
||||
func Set(verbose bool, flag int) {
|
||||
enable = verbose
|
||||
stdlog.SetFlags(flag)
|
||||
}
|
||||
|
||||
// F prints debug log.
|
||||
func F(f string, v ...any) {
|
||||
if enable {
|
||||
stdlog.Output(2, fmt.Sprintf(f, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Print prints log.
|
||||
func Print(v ...any) {
|
||||
stdlog.Print(v...)
|
||||
}
|
||||
|
||||
// Printf prints log.
|
||||
func Printf(f string, v ...any) {
|
||||
stdlog.Printf(f, v...)
|
||||
}
|
||||
|
||||
// Fatal log and exit.
|
||||
func Fatal(v ...any) {
|
||||
stdlog.Fatal(v...)
|
||||
}
|
||||
|
||||
// Fatalf log and exit.
|
||||
func Fatalf(f string, v ...any) {
|
||||
stdlog.Fatalf(f, v...)
|
||||
}
|
51
pkg/pool/buffer.go
Normal file
51
pkg/pool/buffer.go
Normal file
@ -0,0 +1,51 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"math/bits"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// number of pools.
|
||||
num = 17
|
||||
maxsize = 1 << (num - 1)
|
||||
)
|
||||
|
||||
var (
|
||||
sizes [num]int
|
||||
pools [num]sync.Pool
|
||||
)
|
||||
|
||||
func init() {
|
||||
for i := range num {
|
||||
size := 1 << i
|
||||
sizes[i] = size
|
||||
pools[i].New = func() any {
|
||||
buf := make([]byte, size)
|
||||
return unsafe.SliceData(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetBuffer gets a buffer from pool, size should in range: [1, 65536],
|
||||
// otherwise, this function will call make([]byte, size) directly.
|
||||
func GetBuffer(size int) []byte {
|
||||
if size >= 1 && size <= maxsize {
|
||||
i := bits.Len32(uint32(size - 1))
|
||||
if p := pools[i].Get().(*byte); p != nil {
|
||||
return unsafe.Slice(p, 1<<i)[:size]
|
||||
}
|
||||
}
|
||||
return make([]byte, size)
|
||||
}
|
||||
|
||||
// PutBuffer puts a buffer into pool.
|
||||
func PutBuffer(buf []byte) {
|
||||
if size := cap(buf); size >= 1 && size <= maxsize {
|
||||
i := bits.Len32(uint32(size - 1))
|
||||
if sizes[i] == size {
|
||||
pools[i].Put(unsafe.SliceData(buf))
|
||||
}
|
||||
}
|
||||
}
|
24
pkg/pool/bufreader.go
Normal file
24
pkg/pool/bufreader.go
Normal file
@ -0,0 +1,24 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var bufReaderPool sync.Pool
|
||||
|
||||
// GetBufReader returns a *bufio.Reader from pool.
|
||||
func GetBufReader(r io.Reader) *bufio.Reader {
|
||||
if v := bufReaderPool.Get(); v != nil {
|
||||
br := v.(*bufio.Reader)
|
||||
br.Reset(r)
|
||||
return br
|
||||
}
|
||||
return bufio.NewReader(r)
|
||||
}
|
||||
|
||||
// PutBufReader puts a *bufio.Reader into pool.
|
||||
func PutBufReader(br *bufio.Reader) {
|
||||
bufReaderPool.Put(br)
|
||||
}
|
23
pkg/pool/bytesbuffer.go
Normal file
23
pkg/pool/bytesbuffer.go
Normal file
@ -0,0 +1,23 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var bytesBufPool = sync.Pool{
|
||||
New: func() any { return &bytes.Buffer{} },
|
||||
}
|
||||
|
||||
// GetBytesBuffer returns a bytes.buffer from pool.
|
||||
func GetBytesBuffer() *bytes.Buffer {
|
||||
return bytesBufPool.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
// PutBytesBuffer puts a bytes.buffer into pool.
|
||||
func PutBytesBuffer(buf *bytes.Buffer) {
|
||||
if buf.Cap() <= 64<<10 {
|
||||
buf.Reset()
|
||||
bytesBufPool.Put(buf)
|
||||
}
|
||||
}
|
106
pkg/smux/frame.go
Normal file
106
pkg/smux/frame.go
Normal file
@ -0,0 +1,106 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2016-2017 xtaci
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package smux
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const ( // cmds
|
||||
// protocol version 1:
|
||||
cmdSYN byte = iota // stream open
|
||||
cmdFIN // stream close, a.k.a EOF mark
|
||||
cmdPSH // data push
|
||||
cmdNOP // no operation
|
||||
|
||||
// protocol version 2 extra commands
|
||||
// notify bytes consumed by remote peer-end
|
||||
cmdUPD
|
||||
)
|
||||
|
||||
const (
|
||||
// data size of cmdUPD, format:
|
||||
// |4B data consumed(ACK)| 4B window size(WINDOW) |
|
||||
szCmdUPD = 8
|
||||
)
|
||||
|
||||
const (
|
||||
// initial peer window guess, a slow-start
|
||||
initialPeerWindow = 262144
|
||||
)
|
||||
|
||||
const (
|
||||
sizeOfVer = 1
|
||||
sizeOfCmd = 1
|
||||
sizeOfLength = 2
|
||||
sizeOfSid = 4
|
||||
headerSize = sizeOfVer + sizeOfCmd + sizeOfSid + sizeOfLength
|
||||
)
|
||||
|
||||
// Frame defines a packet from or to be multiplexed into a single connection
|
||||
type Frame struct {
|
||||
ver byte // version
|
||||
cmd byte // command
|
||||
sid uint32 // stream id
|
||||
data []byte // payload
|
||||
}
|
||||
|
||||
// newFrame creates a new frame with given version, command and stream id
|
||||
func newFrame(version byte, cmd byte, sid uint32) Frame {
|
||||
return Frame{ver: version, cmd: cmd, sid: sid}
|
||||
}
|
||||
|
||||
// rawHeader is a byte array representation of Frame header
|
||||
type rawHeader [headerSize]byte
|
||||
|
||||
func (h rawHeader) Version() byte {
|
||||
return h[0]
|
||||
}
|
||||
|
||||
func (h rawHeader) Cmd() byte {
|
||||
return h[1]
|
||||
}
|
||||
|
||||
func (h rawHeader) Length() uint16 {
|
||||
return binary.LittleEndian.Uint16(h[2:])
|
||||
}
|
||||
|
||||
func (h rawHeader) StreamID() uint32 {
|
||||
return binary.LittleEndian.Uint32(h[4:])
|
||||
}
|
||||
|
||||
func (h rawHeader) String() string {
|
||||
return fmt.Sprintf("Version:%d Cmd:%d StreamID:%d Length:%d",
|
||||
h.Version(), h.Cmd(), h.StreamID(), h.Length())
|
||||
}
|
||||
|
||||
// updHeader is a byte array representation of cmdUPD
|
||||
type updHeader [szCmdUPD]byte
|
||||
|
||||
func (h updHeader) Consumed() uint32 {
|
||||
return binary.LittleEndian.Uint32(h[:])
|
||||
}
|
||||
func (h updHeader) Window() uint32 {
|
||||
return binary.LittleEndian.Uint32(h[4:])
|
||||
}
|
128
pkg/smux/mux.go
Normal file
128
pkg/smux/mux.go
Normal file
@ -0,0 +1,128 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2016-2017 xtaci
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package smux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config is used to tune the Smux session
|
||||
type Config struct {
|
||||
// SMUX Protocol version, support 1,2
|
||||
Version int
|
||||
|
||||
// Disabled keepalive
|
||||
KeepAliveDisabled bool
|
||||
|
||||
// KeepAliveInterval is how often to send a NOP command to the remote
|
||||
KeepAliveInterval time.Duration
|
||||
|
||||
// KeepAliveTimeout is how long the session
|
||||
// will be closed if no data has arrived
|
||||
KeepAliveTimeout time.Duration
|
||||
|
||||
// MaxFrameSize is used to control the maximum
|
||||
// frame size to sent to the remote
|
||||
MaxFrameSize int
|
||||
|
||||
// MaxReceiveBuffer is used to control the maximum
|
||||
// number of data in the buffer pool
|
||||
MaxReceiveBuffer int
|
||||
|
||||
// MaxStreamBuffer is used to control the maximum
|
||||
// number of data per stream
|
||||
MaxStreamBuffer int
|
||||
}
|
||||
|
||||
// DefaultConfig is used to return a default configuration
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Version: 1,
|
||||
KeepAliveInterval: 10 * time.Second,
|
||||
KeepAliveTimeout: 30 * time.Second,
|
||||
MaxFrameSize: 32768,
|
||||
MaxReceiveBuffer: 4194304,
|
||||
MaxStreamBuffer: 65536,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyConfig is used to verify the sanity of configuration
|
||||
func VerifyConfig(config *Config) error {
|
||||
if !(config.Version == 1 || config.Version == 2) {
|
||||
return errors.New("unsupported protocol version")
|
||||
}
|
||||
if !config.KeepAliveDisabled {
|
||||
if config.KeepAliveInterval == 0 {
|
||||
return errors.New("keep-alive interval must be positive")
|
||||
}
|
||||
if config.KeepAliveTimeout < config.KeepAliveInterval {
|
||||
return fmt.Errorf("keep-alive timeout must be larger than keep-alive interval")
|
||||
}
|
||||
}
|
||||
if config.MaxFrameSize <= 0 {
|
||||
return errors.New("max frame size must be positive")
|
||||
}
|
||||
if config.MaxFrameSize > 65535 {
|
||||
return errors.New("max frame size must not be larger than 65535")
|
||||
}
|
||||
if config.MaxReceiveBuffer <= 0 {
|
||||
return errors.New("max receive buffer must be positive")
|
||||
}
|
||||
if config.MaxStreamBuffer <= 0 {
|
||||
return errors.New("max stream buffer must be positive")
|
||||
}
|
||||
if config.MaxStreamBuffer > config.MaxReceiveBuffer {
|
||||
return errors.New("max stream buffer must not be larger than max receive buffer")
|
||||
}
|
||||
if config.MaxStreamBuffer > math.MaxInt32 {
|
||||
return errors.New("max stream buffer cannot be larger than 2147483647")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Server is used to initialize a new server-side connection.
|
||||
func Server(conn io.ReadWriteCloser, config *Config) (*Session, error) {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
}
|
||||
if err := VerifyConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newSession(config, conn, false), nil
|
||||
}
|
||||
|
||||
// Client is used to initialize a new client-side connection.
|
||||
func Client(conn io.ReadWriteCloser, config *Config) (*Session, error) {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
}
|
||||
|
||||
if err := VerifyConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newSession(config, conn, true), nil
|
||||
}
|
621
pkg/smux/session.go
Normal file
621
pkg/smux/session.go
Normal file
@ -0,0 +1,621 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2016-2017 xtaci
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package smux
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAcceptBacklog = 1024
|
||||
maxShaperSize = 1024
|
||||
openCloseTimeout = 30 * time.Second // Timeout for opening/closing streams
|
||||
)
|
||||
|
||||
// CLASSID represents the class of a frame
|
||||
type CLASSID int
|
||||
|
||||
const (
|
||||
CLSCTRL CLASSID = iota // prioritized control signal
|
||||
CLSDATA
|
||||
)
|
||||
|
||||
// timeoutError representing timeouts for operations such as accept, read and write
|
||||
//
|
||||
// To better cooperate with the standard library, timeoutError should implement the standard library's `net.Error`.
|
||||
//
|
||||
// For example, using smux to implement net.Listener and work with http.Server, the keep-alive connection (*smux.Stream) will be unexpectedly closed.
|
||||
// For more details, see https://github.com/xtaci/smux/pull/99.
|
||||
type timeoutError struct{}
|
||||
|
||||
func (timeoutError) Error() string { return "timeout" }
|
||||
func (timeoutError) Temporary() bool { return true }
|
||||
func (timeoutError) Timeout() bool { return true }
|
||||
|
||||
var (
|
||||
ErrInvalidProtocol = errors.New("invalid protocol")
|
||||
ErrConsumed = errors.New("peer consumed more than sent")
|
||||
ErrGoAway = errors.New("stream id overflows, should start a new connection")
|
||||
ErrTimeout net.Error = &timeoutError{}
|
||||
ErrWouldBlock = errors.New("operation would block on IO")
|
||||
)
|
||||
|
||||
// writeRequest represents a request to write a frame
|
||||
type writeRequest struct {
|
||||
class CLASSID
|
||||
frame Frame
|
||||
seq uint32
|
||||
result chan writeResult
|
||||
}
|
||||
|
||||
// writeResult represents the result of a write request
|
||||
type writeResult struct {
|
||||
n int
|
||||
err error
|
||||
}
|
||||
|
||||
// Session defines a multiplexed connection for streams
|
||||
type Session struct {
|
||||
conn io.ReadWriteCloser
|
||||
|
||||
config *Config
|
||||
nextStreamID uint32 // next stream identifier
|
||||
nextStreamIDLock sync.Mutex
|
||||
|
||||
bucket int32 // token bucket
|
||||
bucketNotify chan struct{} // used for waiting for tokens
|
||||
|
||||
streams map[uint32]*stream // all streams in this session
|
||||
streamLock sync.Mutex // locks streams
|
||||
|
||||
die chan struct{} // flag session has died
|
||||
dieOnce sync.Once
|
||||
|
||||
// socket error handling
|
||||
socketReadError atomic.Value
|
||||
socketWriteError atomic.Value
|
||||
chSocketReadError chan struct{}
|
||||
chSocketWriteError chan struct{}
|
||||
socketReadErrorOnce sync.Once
|
||||
socketWriteErrorOnce sync.Once
|
||||
|
||||
// smux protocol errors
|
||||
protoError atomic.Value
|
||||
chProtoError chan struct{}
|
||||
protoErrorOnce sync.Once
|
||||
|
||||
chAccepts chan *stream
|
||||
|
||||
dataReady int32 // flag data has arrived
|
||||
|
||||
goAway int32 // flag id exhausted
|
||||
|
||||
deadline atomic.Value
|
||||
|
||||
requestID uint32 // Monotonic increasing write request ID
|
||||
shaper chan writeRequest // a shaper for writing
|
||||
writes chan writeRequest
|
||||
}
|
||||
|
||||
func newSession(config *Config, conn io.ReadWriteCloser, client bool) *Session {
|
||||
s := new(Session)
|
||||
s.die = make(chan struct{})
|
||||
s.conn = conn
|
||||
s.config = config
|
||||
s.streams = make(map[uint32]*stream)
|
||||
s.chAccepts = make(chan *stream, defaultAcceptBacklog)
|
||||
s.bucket = int32(config.MaxReceiveBuffer)
|
||||
s.bucketNotify = make(chan struct{}, 1)
|
||||
s.shaper = make(chan writeRequest)
|
||||
s.writes = make(chan writeRequest)
|
||||
s.chSocketReadError = make(chan struct{})
|
||||
s.chSocketWriteError = make(chan struct{})
|
||||
s.chProtoError = make(chan struct{})
|
||||
|
||||
if client {
|
||||
s.nextStreamID = 1
|
||||
} else {
|
||||
s.nextStreamID = 0
|
||||
}
|
||||
|
||||
go s.shaperLoop()
|
||||
go s.recvLoop()
|
||||
go s.sendLoop()
|
||||
if !config.KeepAliveDisabled {
|
||||
go s.keepalive()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// OpenStream is used to create a new stream
|
||||
func (s *Session) OpenStream() (*Stream, error) {
|
||||
if s.IsClosed() {
|
||||
return nil, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
// generate stream id
|
||||
s.nextStreamIDLock.Lock()
|
||||
if s.goAway > 0 {
|
||||
s.nextStreamIDLock.Unlock()
|
||||
return nil, ErrGoAway
|
||||
}
|
||||
|
||||
s.nextStreamID += 2
|
||||
sid := s.nextStreamID
|
||||
if sid == sid%2 { // stream-id overflows
|
||||
s.goAway = 1
|
||||
s.nextStreamIDLock.Unlock()
|
||||
return nil, ErrGoAway
|
||||
}
|
||||
s.nextStreamIDLock.Unlock()
|
||||
|
||||
stream := newStream(sid, s.config.MaxFrameSize, s)
|
||||
|
||||
if _, err := s.writeControlFrame(newFrame(byte(s.config.Version), cmdSYN, sid)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.streamLock.Lock()
|
||||
defer s.streamLock.Unlock()
|
||||
select {
|
||||
case <-s.chSocketReadError:
|
||||
return nil, s.socketReadError.Load().(error)
|
||||
case <-s.chSocketWriteError:
|
||||
return nil, s.socketWriteError.Load().(error)
|
||||
case <-s.die:
|
||||
return nil, io.ErrClosedPipe
|
||||
default:
|
||||
s.streams[sid] = stream
|
||||
wrapper := &Stream{stream: stream}
|
||||
// NOTE(x): disabled finalizer for issue #997
|
||||
/*
|
||||
runtime.SetFinalizer(wrapper, func(s *Stream) {
|
||||
s.Close()
|
||||
})
|
||||
*/
|
||||
return wrapper, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Open returns a generic ReadWriteCloser
|
||||
func (s *Session) Open() (io.ReadWriteCloser, error) {
|
||||
return s.OpenStream()
|
||||
}
|
||||
|
||||
// AcceptStream is used to block until the next available stream
|
||||
// is ready to be accepted.
|
||||
func (s *Session) AcceptStream() (*Stream, error) {
|
||||
var deadline <-chan time.Time
|
||||
if d, ok := s.deadline.Load().(time.Time); ok && !d.IsZero() {
|
||||
timer := time.NewTimer(time.Until(d))
|
||||
defer timer.Stop()
|
||||
deadline = timer.C
|
||||
}
|
||||
|
||||
select {
|
||||
case stream := <-s.chAccepts:
|
||||
wrapper := &Stream{stream: stream}
|
||||
runtime.SetFinalizer(wrapper, func(s *Stream) {
|
||||
s.Close()
|
||||
})
|
||||
return wrapper, nil
|
||||
case <-deadline:
|
||||
return nil, ErrTimeout
|
||||
case <-s.chSocketReadError:
|
||||
return nil, s.socketReadError.Load().(error)
|
||||
case <-s.chProtoError:
|
||||
return nil, s.protoError.Load().(error)
|
||||
case <-s.die:
|
||||
return nil, io.ErrClosedPipe
|
||||
}
|
||||
}
|
||||
|
||||
// Accept Returns a generic ReadWriteCloser instead of smux.Stream
|
||||
func (s *Session) Accept() (io.ReadWriteCloser, error) {
|
||||
return s.AcceptStream()
|
||||
}
|
||||
|
||||
// Close is used to close the session and all streams.
|
||||
func (s *Session) Close() error {
|
||||
var once bool
|
||||
s.dieOnce.Do(func() {
|
||||
close(s.die)
|
||||
once = true
|
||||
})
|
||||
|
||||
if once {
|
||||
s.streamLock.Lock()
|
||||
for k := range s.streams {
|
||||
s.streams[k].sessionClose()
|
||||
}
|
||||
s.streamLock.Unlock()
|
||||
return s.conn.Close()
|
||||
} else {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
}
|
||||
|
||||
// CloseChan can be used by someone who wants to be notified immediately when this
|
||||
// session is closed
|
||||
func (s *Session) CloseChan() <-chan struct{} {
|
||||
return s.die
|
||||
}
|
||||
|
||||
// notifyBucket notifies recvLoop that bucket is available
|
||||
func (s *Session) notifyBucket() {
|
||||
select {
|
||||
case s.bucketNotify <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) notifyReadError(err error) {
|
||||
s.socketReadErrorOnce.Do(func() {
|
||||
s.socketReadError.Store(err)
|
||||
close(s.chSocketReadError)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Session) notifyWriteError(err error) {
|
||||
s.socketWriteErrorOnce.Do(func() {
|
||||
s.socketWriteError.Store(err)
|
||||
close(s.chSocketWriteError)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Session) notifyProtoError(err error) {
|
||||
s.protoErrorOnce.Do(func() {
|
||||
s.protoError.Store(err)
|
||||
close(s.chProtoError)
|
||||
})
|
||||
}
|
||||
|
||||
// IsClosed does a safe check to see if we have shutdown
|
||||
func (s *Session) IsClosed() bool {
|
||||
select {
|
||||
case <-s.die:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// NumStreams returns the number of currently open streams
|
||||
func (s *Session) NumStreams() int {
|
||||
if s.IsClosed() {
|
||||
return 0
|
||||
}
|
||||
s.streamLock.Lock()
|
||||
defer s.streamLock.Unlock()
|
||||
return len(s.streams)
|
||||
}
|
||||
|
||||
// SetDeadline sets a deadline used by Accept* calls.
|
||||
// A zero time value disables the deadline.
|
||||
func (s *Session) SetDeadline(t time.Time) error {
|
||||
s.deadline.Store(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalAddr satisfies net.Conn interface
|
||||
func (s *Session) LocalAddr() net.Addr {
|
||||
if ts, ok := s.conn.(interface {
|
||||
LocalAddr() net.Addr
|
||||
}); ok {
|
||||
return ts.LocalAddr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoteAddr satisfies net.Conn interface
|
||||
func (s *Session) RemoteAddr() net.Addr {
|
||||
if ts, ok := s.conn.(interface {
|
||||
RemoteAddr() net.Addr
|
||||
}); ok {
|
||||
return ts.RemoteAddr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// notify the session that a stream has closed
|
||||
func (s *Session) streamClosed(sid uint32) {
|
||||
s.streamLock.Lock()
|
||||
if stream, ok := s.streams[sid]; ok {
|
||||
n := stream.recycleTokens()
|
||||
if n > 0 { // return remaining tokens to the bucket
|
||||
if atomic.AddInt32(&s.bucket, int32(n)) > 0 {
|
||||
s.notifyBucket()
|
||||
}
|
||||
}
|
||||
delete(s.streams, sid)
|
||||
}
|
||||
s.streamLock.Unlock()
|
||||
}
|
||||
|
||||
// returnTokens is called by stream to return token after read
|
||||
func (s *Session) returnTokens(n int) {
|
||||
if atomic.AddInt32(&s.bucket, int32(n)) > 0 {
|
||||
s.notifyBucket()
|
||||
}
|
||||
}
|
||||
|
||||
// recvLoop keeps on reading from underlying connection if tokens are available
|
||||
func (s *Session) recvLoop() {
|
||||
var hdr rawHeader
|
||||
var updHdr updHeader
|
||||
|
||||
for {
|
||||
for atomic.LoadInt32(&s.bucket) <= 0 && !s.IsClosed() {
|
||||
select {
|
||||
case <-s.bucketNotify:
|
||||
case <-s.die:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// read header first
|
||||
if _, err := io.ReadFull(s.conn, hdr[:]); err == nil {
|
||||
atomic.StoreInt32(&s.dataReady, 1)
|
||||
if hdr.Version() != byte(s.config.Version) {
|
||||
s.notifyProtoError(ErrInvalidProtocol)
|
||||
return
|
||||
}
|
||||
sid := hdr.StreamID()
|
||||
switch hdr.Cmd() {
|
||||
case cmdNOP:
|
||||
case cmdSYN: // stream opening
|
||||
s.streamLock.Lock()
|
||||
if _, ok := s.streams[sid]; !ok {
|
||||
stream := newStream(sid, s.config.MaxFrameSize, s)
|
||||
s.streams[sid] = stream
|
||||
select {
|
||||
case s.chAccepts <- stream:
|
||||
case <-s.die:
|
||||
}
|
||||
}
|
||||
s.streamLock.Unlock()
|
||||
case cmdFIN: // stream closing
|
||||
s.streamLock.Lock()
|
||||
if stream, ok := s.streams[sid]; ok {
|
||||
stream.fin()
|
||||
stream.notifyReadEvent()
|
||||
}
|
||||
s.streamLock.Unlock()
|
||||
case cmdPSH: // data frame
|
||||
if hdr.Length() > 0 {
|
||||
newbuf := pool.GetBuffer(int(hdr.Length()))
|
||||
if written, err := io.ReadFull(s.conn, newbuf); err == nil {
|
||||
s.streamLock.Lock()
|
||||
if stream, ok := s.streams[sid]; ok {
|
||||
stream.pushBytes(newbuf)
|
||||
// a stream used some token
|
||||
atomic.AddInt32(&s.bucket, -int32(written))
|
||||
stream.notifyReadEvent()
|
||||
} else {
|
||||
// data directed to a missing/closed stream, recycle the buffer immediately.
|
||||
pool.PutBuffer(newbuf)
|
||||
}
|
||||
s.streamLock.Unlock()
|
||||
} else {
|
||||
s.notifyReadError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
case cmdUPD: // a window update signal
|
||||
if _, err := io.ReadFull(s.conn, updHdr[:]); err == nil {
|
||||
s.streamLock.Lock()
|
||||
if stream, ok := s.streams[sid]; ok {
|
||||
stream.update(updHdr.Consumed(), updHdr.Window())
|
||||
}
|
||||
s.streamLock.Unlock()
|
||||
} else {
|
||||
s.notifyReadError(err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
s.notifyProtoError(ErrInvalidProtocol)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
s.notifyReadError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keepalive sends NOP frame to peer to keep the connection alive, and detect dead peers
|
||||
func (s *Session) keepalive() {
|
||||
tickerPing := time.NewTicker(s.config.KeepAliveInterval)
|
||||
tickerTimeout := time.NewTicker(s.config.KeepAliveTimeout)
|
||||
defer tickerPing.Stop()
|
||||
defer tickerTimeout.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-tickerPing.C:
|
||||
s.writeFrameInternal(newFrame(byte(s.config.Version), cmdNOP, 0), tickerPing.C, CLSCTRL)
|
||||
s.notifyBucket() // force a signal to the recvLoop
|
||||
case <-tickerTimeout.C:
|
||||
if !atomic.CompareAndSwapInt32(&s.dataReady, 1, 0) {
|
||||
// recvLoop may block while bucket is 0, in this case,
|
||||
// session should not be closed.
|
||||
if atomic.LoadInt32(&s.bucket) > 0 {
|
||||
s.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-s.die:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shaperLoop implements a priority queue for write requests,
|
||||
// some control messages are prioritized over data messages
|
||||
func (s *Session) shaperLoop() {
|
||||
var reqs shaperHeap
|
||||
var next writeRequest
|
||||
var chWrite chan writeRequest
|
||||
var chShaper chan writeRequest
|
||||
|
||||
for {
|
||||
// chWrite is not available until it has packet to send
|
||||
if len(reqs) > 0 {
|
||||
chWrite = s.writes
|
||||
next = heap.Pop(&reqs).(writeRequest)
|
||||
} else {
|
||||
chWrite = nil
|
||||
}
|
||||
|
||||
// control heap size, chShaper is not available until packets are less than maximum allowed
|
||||
if len(reqs) >= maxShaperSize {
|
||||
chShaper = nil
|
||||
} else {
|
||||
chShaper = s.shaper
|
||||
}
|
||||
|
||||
// assertion on non nil
|
||||
if chShaper == nil && chWrite == nil {
|
||||
panic("both channel are nil")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.die:
|
||||
return
|
||||
case r := <-chShaper:
|
||||
if chWrite != nil { // next is valid, reshape
|
||||
heap.Push(&reqs, next)
|
||||
}
|
||||
heap.Push(&reqs, r)
|
||||
case chWrite <- next:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendLoop sends frames to the underlying connection
|
||||
func (s *Session) sendLoop() {
|
||||
var buf []byte
|
||||
var n int
|
||||
var err error
|
||||
var vec [][]byte // vector for writeBuffers
|
||||
|
||||
bw, ok := s.conn.(interface {
|
||||
WriteBuffers(v [][]byte) (n int, err error)
|
||||
})
|
||||
|
||||
if ok {
|
||||
buf = make([]byte, headerSize)
|
||||
vec = make([][]byte, 2)
|
||||
} else {
|
||||
buf = make([]byte, (1<<16)+headerSize)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.die:
|
||||
return
|
||||
case request := <-s.writes:
|
||||
buf[0] = request.frame.ver
|
||||
buf[1] = request.frame.cmd
|
||||
binary.LittleEndian.PutUint16(buf[2:], uint16(len(request.frame.data)))
|
||||
binary.LittleEndian.PutUint32(buf[4:], request.frame.sid)
|
||||
|
||||
// support for scatter-gather I/O
|
||||
if len(vec) > 0 {
|
||||
vec[0] = buf[:headerSize]
|
||||
vec[1] = request.frame.data
|
||||
n, err = bw.WriteBuffers(vec)
|
||||
} else {
|
||||
copy(buf[headerSize:], request.frame.data)
|
||||
n, err = s.conn.Write(buf[:headerSize+len(request.frame.data)])
|
||||
}
|
||||
|
||||
n -= headerSize
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
|
||||
result := writeResult{
|
||||
n: n,
|
||||
err: err,
|
||||
}
|
||||
|
||||
request.result <- result
|
||||
close(request.result)
|
||||
|
||||
// store conn error
|
||||
if err != nil {
|
||||
s.notifyWriteError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeControlFrame writes the control frame to the underlying connection
|
||||
// and returns the number of bytes written if successful
|
||||
func (s *Session) writeControlFrame(f Frame) (n int, err error) {
|
||||
timer := time.NewTimer(openCloseTimeout)
|
||||
defer timer.Stop()
|
||||
|
||||
return s.writeFrameInternal(f, timer.C, CLSCTRL)
|
||||
}
|
||||
|
||||
// internal writeFrame version to support deadline used in keepalive
|
||||
func (s *Session) writeFrameInternal(f Frame, deadline <-chan time.Time, class CLASSID) (int, error) {
|
||||
req := writeRequest{
|
||||
class: class,
|
||||
frame: f,
|
||||
seq: atomic.AddUint32(&s.requestID, 1),
|
||||
result: make(chan writeResult, 1),
|
||||
}
|
||||
select {
|
||||
case s.shaper <- req:
|
||||
case <-s.die:
|
||||
return 0, io.ErrClosedPipe
|
||||
case <-s.chSocketWriteError:
|
||||
return 0, s.socketWriteError.Load().(error)
|
||||
case <-deadline:
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
|
||||
select {
|
||||
case result := <-req.result:
|
||||
return result.n, result.err
|
||||
case <-s.die:
|
||||
return 0, io.ErrClosedPipe
|
||||
case <-s.chSocketWriteError:
|
||||
return 0, s.socketWriteError.Load().(error)
|
||||
case <-deadline:
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
}
|
56
pkg/smux/shaper.go
Normal file
56
pkg/smux/shaper.go
Normal file
@ -0,0 +1,56 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2016-2017 xtaci
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package smux
|
||||
|
||||
// _itimediff returns the time difference between two uint32 values.
|
||||
// The result is a signed 32-bit integer representing the difference between 'later' and 'earlier'.
|
||||
func _itimediff(later, earlier uint32) int32 {
|
||||
return (int32)(later - earlier)
|
||||
}
|
||||
|
||||
// shaperHeap is a min-heap of writeRequest.
|
||||
// It orders writeRequests by class first, then by sequence number within the same class.
|
||||
type shaperHeap []writeRequest
|
||||
|
||||
func (h shaperHeap) Len() int { return len(h) }
|
||||
|
||||
// Less determines the ordering of elements in the heap.
|
||||
// Requests are ordered by their class first. If two requests have the same class,
|
||||
// they are ordered by their sequence numbers.
|
||||
func (h shaperHeap) Less(i, j int) bool {
|
||||
if h[i].class != h[j].class {
|
||||
return h[i].class < h[j].class
|
||||
}
|
||||
return _itimediff(h[j].seq, h[i].seq) > 0
|
||||
}
|
||||
|
||||
func (h shaperHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||
func (h *shaperHeap) Push(x interface{}) { *h = append(*h, x.(writeRequest)) }
|
||||
|
||||
func (h *shaperHeap) Pop() interface{} {
|
||||
old := *h
|
||||
n := len(old)
|
||||
x := old[n-1]
|
||||
*h = old[0 : n-1]
|
||||
return x
|
||||
}
|
615
pkg/smux/stream.go
Normal file
615
pkg/smux/stream.go
Normal file
@ -0,0 +1,615 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2016-2017 xtaci
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package smux
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
)
|
||||
|
||||
// wrapper for GC
|
||||
type Stream struct {
|
||||
*stream
|
||||
}
|
||||
|
||||
// Stream implements net.Conn
|
||||
type stream struct {
|
||||
id uint32 // Stream identifier
|
||||
sess *Session
|
||||
|
||||
buffers [][]byte // the sequential buffers of stream
|
||||
heads [][]byte // slice heads of the buffers above, kept for recycle
|
||||
|
||||
bufferLock sync.Mutex // Mutex to protect access to buffers
|
||||
frameSize int // Maximum frame size for the stream
|
||||
|
||||
// notify a read event
|
||||
chReadEvent chan struct{}
|
||||
|
||||
// flag the stream has closed
|
||||
die chan struct{}
|
||||
dieOnce sync.Once // Ensures die channel is closed only once
|
||||
|
||||
// FIN command
|
||||
chFinEvent chan struct{}
|
||||
finEventOnce sync.Once // Ensures chFinEvent is closed only once
|
||||
|
||||
// deadlines
|
||||
readDeadline atomic.Value
|
||||
writeDeadline atomic.Value
|
||||
|
||||
// per stream sliding window control
|
||||
numRead uint32 // count num of bytes read
|
||||
numWritten uint32 // count num of bytes written
|
||||
incr uint32 // bytes sent since last window update
|
||||
|
||||
// UPD command
|
||||
peerConsumed uint32 // num of bytes the peer has consumed
|
||||
peerWindow uint32 // peer window, initialized to 256KB, updated by peer
|
||||
chUpdate chan struct{} // notify of remote data consuming and window update
|
||||
}
|
||||
|
||||
// newStream initializes and returns a new Stream.
|
||||
func newStream(id uint32, frameSize int, sess *Session) *stream {
|
||||
s := new(stream)
|
||||
s.id = id
|
||||
s.chReadEvent = make(chan struct{}, 1)
|
||||
s.chUpdate = make(chan struct{}, 1)
|
||||
s.frameSize = frameSize
|
||||
s.sess = sess
|
||||
s.die = make(chan struct{})
|
||||
s.chFinEvent = make(chan struct{})
|
||||
s.peerWindow = initialPeerWindow // set to initial window size
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// ID returns the stream's unique identifier.
|
||||
func (s *stream) ID() uint32 {
|
||||
return s.id
|
||||
}
|
||||
|
||||
// Read reads data from the stream into the provided buffer.
|
||||
func (s *stream) Read(b []byte) (n int, err error) {
|
||||
for {
|
||||
n, err = s.tryRead(b)
|
||||
if err == ErrWouldBlock {
|
||||
if ew := s.waitRead(); ew != nil {
|
||||
return 0, ew
|
||||
}
|
||||
} else {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tryRead attempts to read data from the stream without blocking.
|
||||
func (s *stream) tryRead(b []byte) (n int, err error) {
|
||||
if s.sess.config.Version == 2 {
|
||||
return s.tryReadv2(b)
|
||||
}
|
||||
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// A critical section to copy data from buffers to
|
||||
s.bufferLock.Lock()
|
||||
if len(s.buffers) > 0 {
|
||||
n = copy(b, s.buffers[0])
|
||||
s.buffers[0] = s.buffers[0][n:]
|
||||
if len(s.buffers[0]) == 0 {
|
||||
s.buffers[0] = nil
|
||||
s.buffers = s.buffers[1:]
|
||||
// full recycle
|
||||
pool.PutBuffer(s.heads[0])
|
||||
s.heads = s.heads[1:]
|
||||
}
|
||||
}
|
||||
s.bufferLock.Unlock()
|
||||
|
||||
if n > 0 {
|
||||
s.sess.returnTokens(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.die:
|
||||
return 0, io.EOF
|
||||
default:
|
||||
return 0, ErrWouldBlock
|
||||
}
|
||||
}
|
||||
|
||||
// tryReadv2 is the non-blocking version of Read for version 2 streams.
|
||||
func (s *stream) tryReadv2(b []byte) (n int, err error) {
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var notifyConsumed uint32
|
||||
s.bufferLock.Lock()
|
||||
if len(s.buffers) > 0 {
|
||||
n = copy(b, s.buffers[0])
|
||||
s.buffers[0] = s.buffers[0][n:]
|
||||
if len(s.buffers[0]) == 0 {
|
||||
s.buffers[0] = nil
|
||||
s.buffers = s.buffers[1:]
|
||||
// full recycle
|
||||
pool.PutBuffer(s.heads[0])
|
||||
s.heads = s.heads[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// in an ideal environment:
|
||||
// if more than half of buffer has consumed, send read ack to peer
|
||||
// based on round-trip time of ACK, continous flowing data
|
||||
// won't slow down due to waiting for ACK, as long as the
|
||||
// consumer keeps on reading data.
|
||||
//
|
||||
// s.numRead == n implies that it's the initial reading
|
||||
s.numRead += uint32(n)
|
||||
s.incr += uint32(n)
|
||||
|
||||
// for initial reading, send window update
|
||||
if s.incr >= uint32(s.sess.config.MaxStreamBuffer/2) || s.numRead == uint32(n) {
|
||||
notifyConsumed = s.numRead
|
||||
s.incr = 0 // reset couting for next window update
|
||||
}
|
||||
s.bufferLock.Unlock()
|
||||
|
||||
if n > 0 {
|
||||
s.sess.returnTokens(n)
|
||||
|
||||
// send window update if necessary
|
||||
if notifyConsumed > 0 {
|
||||
err := s.sendWindowUpdate(notifyConsumed)
|
||||
return n, err
|
||||
} else {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.die:
|
||||
return 0, io.EOF
|
||||
default:
|
||||
return 0, ErrWouldBlock
|
||||
}
|
||||
}
|
||||
|
||||
// WriteTo implements io.WriteTo
|
||||
// WriteTo writes data to w until there's no more data to write or when an error occurs.
|
||||
// The return value n is the number of bytes written. Any error encountered during the write is also returned.
|
||||
// WriteTo calls Write in a loop until there is no more data to write or when an error occurs.
|
||||
// If the underlying stream is a v2 stream, it will send window update to peer when necessary.
|
||||
// If the underlying stream is a v1 stream, it will not send window update to peer.
|
||||
func (s *stream) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if s.sess.config.Version == 2 {
|
||||
return s.writeTov2(w)
|
||||
}
|
||||
|
||||
for {
|
||||
var buf []byte
|
||||
s.bufferLock.Lock()
|
||||
if len(s.buffers) > 0 {
|
||||
buf = s.buffers[0]
|
||||
s.buffers = s.buffers[1:]
|
||||
s.heads = s.heads[1:]
|
||||
}
|
||||
s.bufferLock.Unlock()
|
||||
|
||||
if buf != nil {
|
||||
nw, ew := w.Write(buf)
|
||||
// NOTE: WriteTo is a reader, so we need to return tokens here
|
||||
s.sess.returnTokens(len(buf))
|
||||
pool.PutBuffer(buf)
|
||||
if nw > 0 {
|
||||
n += int64(nw)
|
||||
}
|
||||
|
||||
if ew != nil {
|
||||
return n, ew
|
||||
}
|
||||
} else if ew := s.waitRead(); ew != nil {
|
||||
return n, ew
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check comments in WriteTo
|
||||
func (s *stream) writeTov2(w io.Writer) (n int64, err error) {
|
||||
for {
|
||||
var notifyConsumed uint32
|
||||
var buf []byte
|
||||
s.bufferLock.Lock()
|
||||
if len(s.buffers) > 0 {
|
||||
buf = s.buffers[0]
|
||||
s.buffers = s.buffers[1:]
|
||||
s.heads = s.heads[1:]
|
||||
}
|
||||
s.numRead += uint32(len(buf))
|
||||
s.incr += uint32(len(buf))
|
||||
if s.incr >= uint32(s.sess.config.MaxStreamBuffer/2) || s.numRead == uint32(len(buf)) {
|
||||
notifyConsumed = s.numRead
|
||||
s.incr = 0
|
||||
}
|
||||
s.bufferLock.Unlock()
|
||||
|
||||
if buf != nil {
|
||||
nw, ew := w.Write(buf)
|
||||
// NOTE: WriteTo is a reader, so we need to return tokens here
|
||||
s.sess.returnTokens(len(buf))
|
||||
pool.PutBuffer(buf)
|
||||
if nw > 0 {
|
||||
n += int64(nw)
|
||||
}
|
||||
|
||||
if ew != nil {
|
||||
return n, ew
|
||||
}
|
||||
|
||||
if notifyConsumed > 0 {
|
||||
if err := s.sendWindowUpdate(notifyConsumed); err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
} else if ew := s.waitRead(); ew != nil {
|
||||
return n, ew
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendWindowUpdate sends a window update frame to the peer.
|
||||
func (s *stream) sendWindowUpdate(consumed uint32) error {
|
||||
var timer *time.Timer
|
||||
var deadline <-chan time.Time
|
||||
if d, ok := s.readDeadline.Load().(time.Time); ok && !d.IsZero() {
|
||||
timer = time.NewTimer(time.Until(d))
|
||||
defer timer.Stop()
|
||||
deadline = timer.C
|
||||
}
|
||||
|
||||
frame := newFrame(byte(s.sess.config.Version), cmdUPD, s.id)
|
||||
var hdr updHeader
|
||||
binary.LittleEndian.PutUint32(hdr[:], consumed)
|
||||
binary.LittleEndian.PutUint32(hdr[4:], uint32(s.sess.config.MaxStreamBuffer))
|
||||
frame.data = hdr[:]
|
||||
_, err := s.sess.writeFrameInternal(frame, deadline, CLSCTRL)
|
||||
return err
|
||||
}
|
||||
|
||||
// waitRead blocks until a read event occurs or a deadline is reached.
|
||||
func (s *stream) waitRead() error {
|
||||
var timer *time.Timer
|
||||
var deadline <-chan time.Time
|
||||
if d, ok := s.readDeadline.Load().(time.Time); ok && !d.IsZero() {
|
||||
timer = time.NewTimer(time.Until(d))
|
||||
defer timer.Stop()
|
||||
deadline = timer.C
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.chReadEvent: // notify some data has arrived, or closed
|
||||
return nil
|
||||
case <-s.chFinEvent:
|
||||
// BUGFIX(xtaci): Fix for https://github.com/xtaci/smux/issues/82
|
||||
s.bufferLock.Lock()
|
||||
defer s.bufferLock.Unlock()
|
||||
if len(s.buffers) > 0 {
|
||||
return nil
|
||||
}
|
||||
return io.EOF
|
||||
case <-s.sess.chSocketReadError:
|
||||
return s.sess.socketReadError.Load().(error)
|
||||
case <-s.sess.chProtoError:
|
||||
return s.sess.protoError.Load().(error)
|
||||
case <-deadline:
|
||||
return ErrTimeout
|
||||
case <-s.die:
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Write implements net.Conn
|
||||
//
|
||||
// Note that the behavior when multiple goroutines write concurrently is not deterministic,
|
||||
// frames may interleave in random way.
|
||||
func (s *stream) Write(b []byte) (n int, err error) {
|
||||
if s.sess.config.Version == 2 {
|
||||
return s.writeV2(b)
|
||||
}
|
||||
|
||||
var deadline <-chan time.Time
|
||||
if d, ok := s.writeDeadline.Load().(time.Time); ok && !d.IsZero() {
|
||||
timer := time.NewTimer(time.Until(d))
|
||||
defer timer.Stop()
|
||||
deadline = timer.C
|
||||
}
|
||||
|
||||
// check if stream has closed
|
||||
select {
|
||||
case <-s.chFinEvent: // passive closing
|
||||
return 0, io.EOF
|
||||
case <-s.die:
|
||||
return 0, io.ErrClosedPipe
|
||||
default:
|
||||
}
|
||||
|
||||
// frame split and transmit
|
||||
sent := 0
|
||||
frame := newFrame(byte(s.sess.config.Version), cmdPSH, s.id)
|
||||
bts := b
|
||||
for len(bts) > 0 {
|
||||
sz := len(bts)
|
||||
if sz > s.frameSize {
|
||||
sz = s.frameSize
|
||||
}
|
||||
frame.data = bts[:sz]
|
||||
bts = bts[sz:]
|
||||
n, err := s.sess.writeFrameInternal(frame, deadline, CLSDATA)
|
||||
s.numWritten++
|
||||
sent += n
|
||||
if err != nil {
|
||||
return sent, err
|
||||
}
|
||||
}
|
||||
|
||||
return sent, nil
|
||||
}
|
||||
|
||||
// writeV2 writes data to the stream for version 2 streams.
|
||||
func (s *stream) writeV2(b []byte) (n int, err error) {
|
||||
// check empty input
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// check if stream has closed
|
||||
select {
|
||||
case <-s.chFinEvent:
|
||||
return 0, io.EOF
|
||||
case <-s.die:
|
||||
return 0, io.ErrClosedPipe
|
||||
default:
|
||||
}
|
||||
|
||||
// create write deadline timer
|
||||
var deadline <-chan time.Time
|
||||
if d, ok := s.writeDeadline.Load().(time.Time); ok && !d.IsZero() {
|
||||
timer := time.NewTimer(time.Until(d))
|
||||
defer timer.Stop()
|
||||
deadline = timer.C
|
||||
}
|
||||
|
||||
// frame split and transmit process
|
||||
sent := 0
|
||||
frame := newFrame(byte(s.sess.config.Version), cmdPSH, s.id)
|
||||
|
||||
for {
|
||||
// per stream sliding window control
|
||||
// [.... [consumed... numWritten] ... win... ]
|
||||
// [.... [consumed...................+rmtwnd]]
|
||||
var bts []byte
|
||||
// note:
|
||||
// even if uint32 overflow, this math still works:
|
||||
// eg1: uint32(0) - uint32(math.MaxUint32) = 1
|
||||
// eg2: int32(uint32(0) - uint32(1)) = -1
|
||||
//
|
||||
// basicially, you can take it as a MODULAR ARITHMETIC
|
||||
inflight := int32(atomic.LoadUint32(&s.numWritten) - atomic.LoadUint32(&s.peerConsumed))
|
||||
if inflight < 0 { // security check for malformed data
|
||||
return 0, ErrConsumed
|
||||
}
|
||||
|
||||
// make sure you understand 'win' is calculated in modular arithmetic(2^32(4GB))
|
||||
win := int32(atomic.LoadUint32(&s.peerWindow)) - inflight
|
||||
|
||||
if win > 0 {
|
||||
// determine how many bytes to send
|
||||
if win > int32(len(b)) {
|
||||
bts = b
|
||||
b = nil
|
||||
} else {
|
||||
bts = b[:win]
|
||||
b = b[win:]
|
||||
}
|
||||
|
||||
// frame split and transmit
|
||||
for len(bts) > 0 {
|
||||
// splitting frame
|
||||
sz := len(bts)
|
||||
if sz > s.frameSize {
|
||||
sz = s.frameSize
|
||||
}
|
||||
frame.data = bts[:sz]
|
||||
bts = bts[sz:]
|
||||
|
||||
// transmit of frame
|
||||
n, err := s.sess.writeFrameInternal(frame, deadline, CLSDATA)
|
||||
atomic.AddUint32(&s.numWritten, uint32(sz))
|
||||
sent += n
|
||||
if err != nil {
|
||||
return sent, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there is any data left to be sent,
|
||||
// wait until stream closes, window changes or deadline reached
|
||||
// this blocking behavior will back propagate flow control to upper layer.
|
||||
if len(b) > 0 {
|
||||
select {
|
||||
case <-s.chFinEvent:
|
||||
return 0, io.EOF
|
||||
case <-s.die:
|
||||
return sent, io.ErrClosedPipe
|
||||
case <-deadline:
|
||||
return sent, ErrTimeout
|
||||
case <-s.sess.chSocketWriteError:
|
||||
return sent, s.sess.socketWriteError.Load().(error)
|
||||
case <-s.chUpdate: // notify of remote data consuming and window update
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
return sent, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close implements net.Conn
|
||||
func (s *stream) Close() error {
|
||||
var once bool
|
||||
var err error
|
||||
s.dieOnce.Do(func() {
|
||||
close(s.die)
|
||||
once = true
|
||||
})
|
||||
|
||||
if once {
|
||||
// send FIN in order
|
||||
f := newFrame(byte(s.sess.config.Version), cmdFIN, s.id)
|
||||
|
||||
timer := time.NewTimer(openCloseTimeout)
|
||||
defer timer.Stop()
|
||||
|
||||
_, err = s.sess.writeFrameInternal(f, timer.C, CLSDATA)
|
||||
s.sess.streamClosed(s.id)
|
||||
return err
|
||||
} else {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
}
|
||||
|
||||
// GetDieCh returns a readonly chan which can be readable
|
||||
// when the stream is to be closed.
|
||||
func (s *stream) GetDieCh() <-chan struct{} {
|
||||
return s.die
|
||||
}
|
||||
|
||||
// SetReadDeadline sets the read deadline as defined by
|
||||
// net.Conn.SetReadDeadline.
|
||||
// A zero time value disables the deadline.
|
||||
func (s *stream) SetReadDeadline(t time.Time) error {
|
||||
s.readDeadline.Store(t)
|
||||
s.notifyReadEvent()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWriteDeadline sets the write deadline as defined by
|
||||
// net.Conn.SetWriteDeadline.
|
||||
// A zero time value disables the deadline.
|
||||
func (s *stream) SetWriteDeadline(t time.Time) error {
|
||||
s.writeDeadline.Store(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeadline sets both read and write deadlines as defined by
|
||||
// net.Conn.SetDeadline.
|
||||
// A zero time value disables the deadlines.
|
||||
func (s *stream) SetDeadline(t time.Time) error {
|
||||
if err := s.SetReadDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.SetWriteDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// session closes
|
||||
func (s *stream) sessionClose() { s.dieOnce.Do(func() { close(s.die) }) }
|
||||
|
||||
// LocalAddr satisfies net.Conn interface
|
||||
func (s *stream) LocalAddr() net.Addr {
|
||||
if ts, ok := s.sess.conn.(interface {
|
||||
LocalAddr() net.Addr
|
||||
}); ok {
|
||||
return ts.LocalAddr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoteAddr satisfies net.Conn interface
|
||||
func (s *stream) RemoteAddr() net.Addr {
|
||||
if ts, ok := s.sess.conn.(interface {
|
||||
RemoteAddr() net.Addr
|
||||
}); ok {
|
||||
return ts.RemoteAddr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushBytes append buf to buffers
|
||||
func (s *stream) pushBytes(buf []byte) (written int, err error) {
|
||||
s.bufferLock.Lock()
|
||||
s.buffers = append(s.buffers, buf)
|
||||
s.heads = append(s.heads, buf)
|
||||
s.bufferLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// recycleTokens transform remaining bytes to tokens(will truncate buffer)
|
||||
func (s *stream) recycleTokens() (n int) {
|
||||
s.bufferLock.Lock()
|
||||
for k := range s.buffers {
|
||||
n += len(s.buffers[k])
|
||||
pool.PutBuffer(s.heads[k])
|
||||
}
|
||||
s.buffers = nil
|
||||
s.heads = nil
|
||||
s.bufferLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// notify read event
|
||||
func (s *stream) notifyReadEvent() {
|
||||
select {
|
||||
case s.chReadEvent <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// update command
|
||||
func (s *stream) update(consumed uint32, window uint32) {
|
||||
atomic.StoreUint32(&s.peerConsumed, consumed)
|
||||
atomic.StoreUint32(&s.peerWindow, window)
|
||||
select {
|
||||
case s.chUpdate <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// mark this stream has been closed in protocol
|
||||
func (s *stream) fin() {
|
||||
s.finEventOnce.Do(func() {
|
||||
close(s.chFinEvent)
|
||||
})
|
||||
}
|
31
pkg/sockopt/sockopt.go
Normal file
31
pkg/sockopt/sockopt.go
Normal file
@ -0,0 +1,31 @@
|
||||
package sockopt
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Options is the options struct.
|
||||
type Options struct {
|
||||
bindIface *net.Interface
|
||||
reuseAddr bool
|
||||
}
|
||||
|
||||
// Option is the function paramater.
|
||||
type Option func(opts *Options)
|
||||
|
||||
// Bind sets the bind interface option.
|
||||
func Bind(intf *net.Interface) Option { return func(opts *Options) { opts.bindIface = intf } }
|
||||
|
||||
// ReuseAddr sets the reuse addr option.
|
||||
func ReuseAddr() Option { return func(opts *Options) { opts.reuseAddr = true } }
|
||||
|
||||
// Control returns a control function for the net.Dialer and net.ListenConfig.
|
||||
func Control(opts ...Option) func(network, address string, c syscall.RawConn) error {
|
||||
option := &Options{}
|
||||
for _, opt := range opts {
|
||||
opt(option)
|
||||
}
|
||||
|
||||
return control(option)
|
||||
}
|
28
pkg/sockopt/sockopt_darwin.go
Normal file
28
pkg/sockopt/sockopt_darwin.go
Normal file
@ -0,0 +1,28 @@
|
||||
package sockopt
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func control(opt *Options) func(network, address string, c syscall.RawConn) error {
|
||||
return func(network, address string, c syscall.RawConn) (err error) {
|
||||
return c.Control(func(fd uintptr) {
|
||||
|
||||
if opt.bindIface != nil {
|
||||
switch network {
|
||||
case "tcp4", "udp4":
|
||||
unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, opt.bindIface.Index)
|
||||
case "tcp6", "udp6":
|
||||
unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, opt.bindIface.Index)
|
||||
}
|
||||
}
|
||||
if opt.reuseAddr {
|
||||
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
|
||||
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
23
pkg/sockopt/sockopt_linux.go
Normal file
23
pkg/sockopt/sockopt_linux.go
Normal file
@ -0,0 +1,23 @@
|
||||
package sockopt
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func control(opt *Options) func(network, address string, c syscall.RawConn) error {
|
||||
return func(network, address string, c syscall.RawConn) (err error) {
|
||||
return c.Control(func(fd uintptr) {
|
||||
|
||||
if opt.bindIface != nil {
|
||||
unix.BindToDevice(int(fd), opt.bindIface.Name)
|
||||
}
|
||||
if opt.reuseAddr {
|
||||
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
|
||||
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
9
pkg/sockopt/sockopt_others.go
Normal file
9
pkg/sockopt/sockopt_others.go
Normal file
@ -0,0 +1,9 @@
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package sockopt
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func control(opt *Options) func(string, string, syscall.RawConn) error { return nil }
|
164
pkg/socks/socks.go
Normal file
164
pkg/socks/socks.go
Normal file
@ -0,0 +1,164 @@
|
||||
package socks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SOCKS auth type
|
||||
const (
|
||||
AuthNone = 0
|
||||
AuthPassword = 2
|
||||
)
|
||||
|
||||
// SOCKS request commands as defined in RFC 1928 section 4
|
||||
const (
|
||||
CmdError byte = 0
|
||||
CmdConnect byte = 1
|
||||
CmdBind byte = 2
|
||||
CmdUDPAssociate byte = 3
|
||||
)
|
||||
|
||||
// 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
|
||||
const MaxAddrLen = 1 + 1 + 255 + 2
|
||||
|
||||
// Errors are socks5 errors
|
||||
var Errors = []error{
|
||||
errors.New(""),
|
||||
errors.New("general failure"),
|
||||
errors.New("connection forbidden"),
|
||||
errors.New("network unreachable"),
|
||||
errors.New("host unreachable"),
|
||||
errors.New("connection refused"),
|
||||
errors.New("TTL expired"),
|
||||
errors.New("command not supported"),
|
||||
errors.New("address type not supported"),
|
||||
errors.New("socks5UDPAssociate"),
|
||||
}
|
||||
|
||||
// Addr represents a SOCKS address as defined in RFC 1928 section 5.
|
||||
type Addr []byte
|
||||
|
||||
// String serializes SOCKS address a to string form.
|
||||
func (a Addr) String() string {
|
||||
var host, port string
|
||||
|
||||
switch 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]))
|
||||
case ATypIP4:
|
||||
host = net.IP(a[1 : 1+net.IPv4len]).String()
|
||||
port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1]))
|
||||
case ATypIP6:
|
||||
host = net.IP(a[1 : 1+net.IPv6len]).String()
|
||||
port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1]))
|
||||
}
|
||||
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
// Network returns network name. Implements net.Addr interface.
|
||||
func (a Addr) Network() string { return "socks" }
|
||||
|
||||
// ReadAddr reads just enough bytes from r to get a valid Addr.
|
||||
func ReadAddr(r io.Reader) (Addr, error) {
|
||||
b := make([]byte, MaxAddrLen)
|
||||
_, err := io.ReadFull(r, b[:1]) // read 1st byte for address type
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch b[0] {
|
||||
case ATypDomain:
|
||||
_, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = io.ReadFull(r, b[2:2+int(b[1])+2])
|
||||
return b[:1+1+int(b[1])+2], err
|
||||
case ATypIP4:
|
||||
_, err = io.ReadFull(r, b[1:1+net.IPv4len+2])
|
||||
return b[:1+net.IPv4len+2], err
|
||||
case ATypIP6:
|
||||
_, err = io.ReadFull(r, b[1:1+net.IPv6len+2])
|
||||
return b[:1+net.IPv6len+2], err
|
||||
}
|
||||
|
||||
return nil, Errors[8]
|
||||
}
|
||||
|
||||
// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed.
|
||||
func SplitAddr(b []byte) Addr {
|
||||
addrLen := 1
|
||||
if len(b) < addrLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch b[0] {
|
||||
case ATypDomain:
|
||||
if len(b) < 2 {
|
||||
return nil
|
||||
}
|
||||
addrLen = 1 + 1 + int(b[1]) + 2
|
||||
case ATypIP4:
|
||||
addrLen = 1 + net.IPv4len + 2
|
||||
case ATypIP6:
|
||||
addrLen = 1 + net.IPv6len + 2
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(b) < addrLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b[:addrLen]
|
||||
}
|
||||
|
||||
// ParseAddr parses the address in string s. Returns nil if failed.
|
||||
func ParseAddr(s string) Addr {
|
||||
var addr Addr
|
||||
host, port, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ip, err := netip.ParseAddr(host); err == nil {
|
||||
if ip.Is4() {
|
||||
addr = make([]byte, 1+net.IPv4len+2)
|
||||
addr[0] = ATypIP4
|
||||
} else {
|
||||
addr = make([]byte, 1+net.IPv6len+2)
|
||||
addr[0] = ATypIP6
|
||||
}
|
||||
copy(addr[1:], ip.AsSlice())
|
||||
} else {
|
||||
if len(host) > 255 {
|
||||
return nil
|
||||
}
|
||||
addr = make([]byte, 1+1+len(host)+2)
|
||||
addr[0] = ATypDomain
|
||||
addr[1] = byte(len(host))
|
||||
copy(addr[2:], host)
|
||||
}
|
||||
|
||||
portnum, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum)
|
||||
|
||||
return addr
|
||||
}
|
206
proxy/conn.go
Normal file
206
proxy/conn.go
Normal file
@ -0,0 +1,206 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
)
|
||||
|
||||
var (
|
||||
// TCPBufSize is the size of tcp buffer.
|
||||
TCPBufSize = 32 << 10
|
||||
|
||||
// UDPBufSize is the size of udp buffer.
|
||||
UDPBufSize = 2 << 10
|
||||
)
|
||||
|
||||
// Conn is a connection with buffered reader.
|
||||
type Conn struct {
|
||||
r *bufio.Reader
|
||||
net.Conn
|
||||
}
|
||||
|
||||
// NewConn returns a new conn.
|
||||
func NewConn(c net.Conn) *Conn {
|
||||
if conn, ok := c.(*Conn); ok {
|
||||
return conn
|
||||
}
|
||||
return &Conn{pool.GetBufReader(c), c}
|
||||
}
|
||||
|
||||
// Reader returns the internal bufio.Reader.
|
||||
func (c *Conn) Reader() *bufio.Reader { return c.r }
|
||||
func (c *Conn) Read(p []byte) (int, error) { return c.r.Read(p) }
|
||||
|
||||
// Peek returns the next n bytes without advancing the reader.
|
||||
func (c *Conn) Peek(n int) ([]byte, error) { return c.r.Peek(n) }
|
||||
|
||||
// WriteTo implements io.WriterTo.
|
||||
func (c *Conn) WriteTo(w io.Writer) (n int64, err error) { return c.r.WriteTo(w) }
|
||||
|
||||
// Close closes the Conn.
|
||||
func (c *Conn) Close() error {
|
||||
pool.PutBufReader(c.r)
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
// Relay relays between left and right.
|
||||
func Relay(left, right net.Conn) error {
|
||||
var err, err1 error
|
||||
var wg sync.WaitGroup
|
||||
var wait = 5 * time.Second
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, err1 = Copy(right, left)
|
||||
right.SetReadDeadline(time.Now().Add(wait)) // unblock read on right
|
||||
}()
|
||||
|
||||
_, err = Copy(left, right)
|
||||
left.SetReadDeadline(time.Now().Add(wait)) // unblock read on left
|
||||
wg.Wait()
|
||||
|
||||
if err1 != nil && !errors.Is(err1, os.ErrDeadlineExceeded) {
|
||||
return err1
|
||||
}
|
||||
|
||||
if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy copies from src to dst.
|
||||
func Copy(dst io.Writer, src io.Reader) (written int64, err error) {
|
||||
dst = underlyingWriter(dst)
|
||||
switch runtime.GOOS {
|
||||
case "linux", "windows", "dragonfly", "freebsd", "solaris":
|
||||
if _, ok := dst.(*net.TCPConn); ok && worthTry(src) {
|
||||
if wt, ok := src.(io.WriterTo); ok {
|
||||
return wt.WriteTo(dst)
|
||||
}
|
||||
if rt, ok := dst.(io.ReaderFrom); ok {
|
||||
return rt.ReadFrom(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
return CopyBuffer(dst, src)
|
||||
}
|
||||
|
||||
func underlyingWriter(c io.Writer) io.Writer {
|
||||
if wrap, ok := c.(*Conn); ok {
|
||||
return wrap.Conn
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func worthTry(src io.Reader) bool {
|
||||
switch v := src.(type) {
|
||||
case *net.TCPConn, *net.UnixConn:
|
||||
return true
|
||||
case *io.LimitedReader:
|
||||
return worthTry(v.R)
|
||||
case *Conn:
|
||||
return worthTry(v.Conn)
|
||||
case *os.File:
|
||||
fi, err := v.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fi.Mode().IsRegular()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// CopyN copies n bytes (or until an error) from src to dst.
|
||||
func CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||
written, err = Copy(dst, io.LimitReader(src, n))
|
||||
if written == n {
|
||||
return n, nil
|
||||
}
|
||||
if written < n && err == nil {
|
||||
// src stopped early; must have been EOF.
|
||||
err = io.EOF
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CopyBuffer copies from src to dst with a userspace buffer.
|
||||
func CopyBuffer(dst io.Writer, src io.Reader) (written int64, err error) {
|
||||
size := TCPBufSize
|
||||
if l, ok := src.(*io.LimitedReader); ok && int64(size) > l.N {
|
||||
if l.N < 1 {
|
||||
size = 1
|
||||
} else {
|
||||
size = int(l.N)
|
||||
}
|
||||
}
|
||||
|
||||
buf := pool.GetBuffer(size)
|
||||
defer pool.PutBuffer(buf)
|
||||
|
||||
for {
|
||||
nr, er := src.Read(buf)
|
||||
if nr > 0 {
|
||||
nw, ew := dst.Write(buf[0:nr])
|
||||
if nw > 0 {
|
||||
written += int64(nw)
|
||||
}
|
||||
if ew != nil {
|
||||
err = ew
|
||||
break
|
||||
}
|
||||
if nr != nw {
|
||||
err = io.ErrShortWrite
|
||||
break
|
||||
}
|
||||
}
|
||||
if er != nil {
|
||||
if er != io.EOF {
|
||||
err = er
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
||||
// CopyUDP copys from src to dst at target with read timeout.
|
||||
// if step sets to non-zero value,
|
||||
// the read timeout will be increased from 0 to timeout by step in every read operation.
|
||||
func CopyUDP(dst net.PacketConn, writeTo net.Addr, src net.PacketConn, timeout time.Duration, step time.Duration) error {
|
||||
buf := pool.GetBuffer(UDPBufSize)
|
||||
defer pool.PutBuffer(buf)
|
||||
|
||||
var t time.Duration
|
||||
for {
|
||||
if t += step; t == 0 || t > timeout {
|
||||
t = timeout
|
||||
}
|
||||
|
||||
src.SetReadDeadline(time.Now().Add(t))
|
||||
n, addr, err := src.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if writeTo != nil {
|
||||
addr = writeTo
|
||||
}
|
||||
|
||||
_, err = dst.WriteTo(buf[:n], addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
79
proxy/dialer.go
Normal file
79
proxy/dialer.go
Normal file
@ -0,0 +1,79 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotSupported indicates that the operation is not supported
|
||||
ErrNotSupported = errors.New("not supported")
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// DialerCreator is a function to create dialers.
|
||||
type DialerCreator func(s string, dialer Dialer) (Dialer, error)
|
||||
|
||||
var (
|
||||
dialerCreators = make(map[string]DialerCreator)
|
||||
)
|
||||
|
||||
// RegisterDialer is used to register a dialer.
|
||||
func RegisterDialer(name string, c DialerCreator) {
|
||||
dialerCreators[strings.ToLower(name)] = c
|
||||
}
|
||||
|
||||
// DialerFromURL calls the registered creator to create dialers.
|
||||
// dialer is the default upstream dialer so cannot be nil, we can use Default when calling this function.
|
||||
func DialerFromURL(s string, dialer Dialer) (Dialer, error) {
|
||||
if dialer == nil {
|
||||
return nil, errors.New("DialerFromURL: dialer cannot be nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(s, "://") {
|
||||
s = s + "://"
|
||||
}
|
||||
|
||||
scheme := s[:strings.Index(s, ":")]
|
||||
c, ok := dialerCreators[strings.ToLower(scheme)]
|
||||
if ok {
|
||||
return c(s, dialer)
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown scheme '" + 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, " ")
|
||||
}
|
151
proxy/direct.go
Normal file
151
proxy/direct.go
Normal file
@ -0,0 +1,151 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/nadoo/glider/pkg/sockopt"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// NewDirect returns a Direct dialer.
|
||||
func NewDirect(intface string, dialTimeout, relayTimeout time.Duration) (*Direct, error) {
|
||||
d := &Direct{dialTimeout: dialTimeout, relayTimeout: relayTimeout}
|
||||
|
||||
if intface != "" {
|
||||
if addr, err := netip.ParseAddr(intface); err == nil {
|
||||
d.ip = addr.AsSlice()
|
||||
} else {
|
||||
iface, err := net.InterfaceByName(intface)
|
||||
if err != nil {
|
||||
return nil, errors.New(err.Error() + ": " + intface)
|
||||
}
|
||||
d.iface = iface
|
||||
}
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewDirectDialer returns a direct dialer.
|
||||
func NewDirectDialer(s string, d Dialer) (Dialer, error) {
|
||||
if d == nil {
|
||||
return NewDirect("", time.Duration(3)*time.Second, time.Duration(3)*time.Second)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Addr returns forwarder's address.
|
||||
func (d *Direct) Addr() string { return "DIRECT" }
|
||||
|
||||
// Dial connects to the address addr on the network net
|
||||
func (d *Direct) Dial(network, addr string) (c net.Conn, err error) {
|
||||
if d.iface == nil || d.ip != nil {
|
||||
c, err = d.dial(network, addr, d.ip)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, ip := range d.IFaceIPs() {
|
||||
c, err = d.dial(network, addr, ip)
|
||||
if err == nil {
|
||||
d.ip = ip
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// no ip available (so no dials made), maybe the interface link is down
|
||||
if c == nil && err == nil {
|
||||
err = errors.New("dial failed, maybe the interface link is down, please check it")
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (d *Direct) dial(network, addr string, localIP net.IP) (net.Conn, error) {
|
||||
var la net.Addr
|
||||
switch network {
|
||||
case "tcp":
|
||||
la = &net.TCPAddr{IP: localIP}
|
||||
case "udp":
|
||||
la = &net.UDPAddr{IP: localIP}
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{LocalAddr: la, Timeout: d.dialTimeout}
|
||||
if d.iface != nil {
|
||||
dialer.Control = sockopt.Control(sockopt.Bind(d.iface))
|
||||
}
|
||||
|
||||
c, err := dialer.Dial(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c, ok := c.(*net.TCPConn); ok {
|
||||
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
|
||||
if d.ip != nil {
|
||||
la = net.JoinHostPort(d.ip.String(), "0")
|
||||
}
|
||||
|
||||
lc := &net.ListenConfig{}
|
||||
if d.iface != nil {
|
||||
lc.Control = sockopt.Control(sockopt.Bind(d.iface))
|
||||
}
|
||||
|
||||
return lc.ListenPacket(context.Background(), network, la)
|
||||
}
|
||||
|
||||
// IFaceIPs returns ip addresses according to the specified interface.
|
||||
func (d *Direct) IFaceIPs() (ips []net.IP) {
|
||||
ipNets, err := d.iface.Addrs()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
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
|
||||
`)
|
||||
}
|
82
proxy/http/client.go
Normal file
82
proxy/http/client.go
Normal file
@ -0,0 +1,82 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"net/textproto"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// NewHTTPDialer returns a http proxy dialer.
|
||||
func NewHTTPDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
|
||||
return NewHTTP(s, d, nil)
|
||||
}
|
||||
|
||||
// Addr returns forwarder's address.
|
||||
func (s *HTTP) Addr() string {
|
||||
if s.addr == "" {
|
||||
return s.dialer.Addr()
|
||||
}
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the network net via the proxy.
|
||||
func (s *HTTP) Dial(network, addr string) (net.Conn, error) {
|
||||
rc, err := s.dialer.Dial(network, s.addr)
|
||||
if err != nil {
|
||||
log.F("[http] dial to %s error: %s", s.addr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := pool.GetBytesBuffer()
|
||||
defer pool.PutBytesBuffer(buf)
|
||||
|
||||
buf.WriteString("CONNECT " + addr + " HTTP/1.1\r\n")
|
||||
buf.WriteString("Host: " + addr + "\r\n")
|
||||
buf.WriteString("Proxy-Connection: Keep-Alive\r\n")
|
||||
|
||||
if s.user != "" && s.password != "" {
|
||||
auth := s.user + ":" + s.password
|
||||
buf.WriteString("Proxy-Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + "\r\n")
|
||||
}
|
||||
|
||||
// header ended
|
||||
buf.WriteString("\r\n")
|
||||
_, err = rc.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := proxy.NewConn(rc)
|
||||
tpr := textproto.NewReader(c.Reader())
|
||||
line, err := tpr.ReadLine()
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
_, code, _, ok := parseStartLine(line)
|
||||
if ok && code == "200" {
|
||||
tpr.ReadMIMEHeader()
|
||||
return c, err
|
||||
}
|
||||
|
||||
switch code {
|
||||
case "403":
|
||||
log.F("[http] 'CONNECT' to ports other than 443 are not allowed by proxy %s", s.addr)
|
||||
case "405":
|
||||
log.F("[http] 'CONNECT' method not allowed by proxy %s", s.addr)
|
||||
case "407":
|
||||
log.F("[http] authencation needed by proxy %s", s.addr)
|
||||
}
|
||||
|
||||
return nil, errors.New("[http] can not connect remote address: " + addr + ". error code: " + code)
|
||||
}
|
||||
|
||||
// DialUDP connects to the given address via the proxy.
|
||||
func (s *HTTP) DialUDP(network, addr string) (pc net.PacketConn, err error) {
|
||||
return nil, proxy.ErrNotSupported
|
||||
}
|
121
proxy/http/http.go
Normal file
121
proxy/http/http.go
Normal file
@ -0,0 +1,121 @@
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
|
||||
// NOTE: never keep-alive so the implementation can be much easier.
|
||||
|
||||
// Package http implements a http proxy.
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// HTTP struct.
|
||||
type HTTP struct {
|
||||
dialer proxy.Dialer
|
||||
proxy proxy.Proxy
|
||||
addr string
|
||||
user string
|
||||
password string
|
||||
pretend bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.RegisterDialer("http", NewHTTPDialer)
|
||||
proxy.RegisterServer("http", NewHTTPServer)
|
||||
}
|
||||
|
||||
// NewHTTP returns a http proxy.
|
||||
func NewHTTP(s string, d proxy.Dialer, p proxy.Proxy) (*HTTP, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
log.F("parse err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := u.Host
|
||||
user := u.User.Username()
|
||||
pass, _ := u.User.Password()
|
||||
|
||||
h := &HTTP{
|
||||
dialer: d,
|
||||
proxy: p,
|
||||
addr: addr,
|
||||
user: user,
|
||||
password: pass,
|
||||
pretend: false,
|
||||
}
|
||||
|
||||
if u.Query().Get("pretend") == "true" {
|
||||
h.pretend = true
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// parseStartLine parses "GET /foo HTTP/1.1" OR "HTTP/1.1 200 OK" into its three parts.
|
||||
func parseStartLine(line string) (r1, r2, r3 string, ok bool) {
|
||||
s1 := strings.Index(line, " ")
|
||||
s2 := strings.Index(line[s1+1:], " ")
|
||||
if s1 < 0 || s2 < 0 {
|
||||
return
|
||||
}
|
||||
s2 += s1 + 1
|
||||
return line[:s1], line[s1+1 : s2], line[s2+1:], true
|
||||
}
|
||||
|
||||
func cleanHeaders(header textproto.MIMEHeader) {
|
||||
header.Del("Proxy-Connection")
|
||||
header.Del("Connection")
|
||||
header.Del("Keep-Alive")
|
||||
header.Del("Proxy-Authenticate")
|
||||
header.Del("Proxy-Authorization")
|
||||
header.Del("TE")
|
||||
header.Del("Trailers")
|
||||
header.Del("Transfer-Encoding")
|
||||
header.Del("Upgrade")
|
||||
}
|
||||
|
||||
func writeStartLine(w io.Writer, s1, s2, s3 string) {
|
||||
io.WriteString(w, s1+" "+s2+" "+s3+"\r\n")
|
||||
}
|
||||
|
||||
func writeHeaders(w io.Writer, header textproto.MIMEHeader) {
|
||||
for key, values := range header {
|
||||
for _, v := range values {
|
||||
io.WriteString(w, key+": "+v+"\r\n")
|
||||
}
|
||||
}
|
||||
io.WriteString(w, "\r\n")
|
||||
}
|
||||
|
||||
func extractUserPass(auth string) (username, password string, ok bool) {
|
||||
if !strings.HasPrefix(auth, "Basic ") {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic "))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s := string(b)
|
||||
idx := strings.IndexByte(s, ':')
|
||||
if idx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
return s[:idx], s[idx+1:], true
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.AddUsage("http", `
|
||||
Http scheme:
|
||||
http://[user:pass@]host:port
|
||||
`)
|
||||
}
|
120
proxy/http/request.go
Normal file
120
proxy/http/request.go
Normal file
@ -0,0 +1,120 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
)
|
||||
|
||||
// Methods are http methods from rfc.
|
||||
// https://www.rfc-editor.org/rfc/rfc2616, http methods must be uppercase
|
||||
var Methods = [...][]byte{
|
||||
[]byte("GET"),
|
||||
[]byte("POST"),
|
||||
[]byte("PUT"),
|
||||
[]byte("DELETE"),
|
||||
[]byte("CONNECT"),
|
||||
[]byte("HEAD"),
|
||||
[]byte("OPTIONS"),
|
||||
[]byte("TRACE"),
|
||||
[]byte("PATCH"),
|
||||
}
|
||||
|
||||
type request struct {
|
||||
method string
|
||||
uri string
|
||||
proto string
|
||||
auth string
|
||||
header textproto.MIMEHeader
|
||||
|
||||
target string // target host with port
|
||||
ruri string // relative uri
|
||||
absuri string // absolute uri
|
||||
}
|
||||
|
||||
func parseRequest(r *bufio.Reader) (*request, error) {
|
||||
tpr := textproto.NewReader(r)
|
||||
line, err := tpr.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method, uri, proto, ok := parseStartLine(line)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error in parseStartLine: %s", line)
|
||||
}
|
||||
|
||||
header, err := tpr.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
log.F("[http] read header error:%s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth := header.Get("Proxy-Authorization")
|
||||
|
||||
cleanHeaders(header)
|
||||
header.Set("Connection", "close")
|
||||
|
||||
// https://github.com/golang/go/blob/dcf0929de6a12103a8fd7097abd6e797188c366d/src/net/http/request.go#L1047
|
||||
justAuthority := method == "CONNECT" && !strings.HasPrefix(uri, "/")
|
||||
if justAuthority {
|
||||
uri = "http://" + uri
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(uri)
|
||||
if err != nil {
|
||||
log.F("[http] parse request url error: %s, uri: %s", err, uri)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if justAuthority {
|
||||
// Strip the bogus "http://" back off.
|
||||
u.Scheme = ""
|
||||
uri = uri[7:]
|
||||
}
|
||||
|
||||
tgt := u.Host
|
||||
if !strings.Contains(tgt, ":") {
|
||||
tgt += ":80"
|
||||
}
|
||||
|
||||
req := &request{
|
||||
method: method,
|
||||
uri: uri,
|
||||
proto: proto,
|
||||
auth: auth,
|
||||
header: header,
|
||||
target: tgt,
|
||||
}
|
||||
|
||||
if u.IsAbs() {
|
||||
req.absuri = u.String()
|
||||
u.Scheme, u.Host = "", ""
|
||||
req.ruri = u.String()
|
||||
} else {
|
||||
req.ruri = u.String()
|
||||
base, err := url.Parse("http://" + header.Get("Host"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u = base.ResolveReference(u)
|
||||
req.absuri = u.String()
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (r *request) WriteBuf(buf *bytes.Buffer) {
|
||||
writeStartLine(buf, r.method, r.ruri, r.proto)
|
||||
writeHeaders(buf, r.header)
|
||||
}
|
||||
|
||||
func (r *request) WriteAbsBuf(buf *bytes.Buffer) {
|
||||
writeStartLine(buf, r.method, r.absuri, r.proto)
|
||||
writeHeaders(buf, r.header)
|
||||
}
|
166
proxy/http/server.go
Normal file
166
proxy/http/server.go
Normal file
@ -0,0 +1,166 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// NewHTTPServer returns a http proxy server.
|
||||
func NewHTTPServer(s string, p proxy.Proxy) (proxy.Server, error) {
|
||||
return NewHTTP(s, nil, p)
|
||||
}
|
||||
|
||||
// ListenAndServe listens on server's addr and serves connections.
|
||||
func (s *HTTP) ListenAndServe() {
|
||||
l, err := net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.Fatalf("[http] failed to listen on %s: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
log.F("[http] listening TCP on %s", s.addr)
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
log.F("[http] failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go s.Serve(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve serves a connection.
|
||||
func (s *HTTP) Serve(cc net.Conn) {
|
||||
if c, ok := cc.(*net.TCPConn); ok {
|
||||
c.SetKeepAlive(true)
|
||||
}
|
||||
|
||||
c := proxy.NewConn(cc)
|
||||
defer c.Close()
|
||||
|
||||
req, err := parseRequest(c.Reader())
|
||||
if err != nil {
|
||||
log.F("[http] can not parse request from %s, error: %v", c.RemoteAddr(), err)
|
||||
return
|
||||
}
|
||||
|
||||
if s.pretend {
|
||||
fmt.Fprintf(c, "%s 404 Not Found\r\nServer: nginx\r\n\r\n404 Not Found\r\n", req.proto)
|
||||
log.F("[http] %s <-> %s, pretend as web server", c.RemoteAddr().String(), s.Addr())
|
||||
return
|
||||
}
|
||||
|
||||
s.servRequest(req, c)
|
||||
}
|
||||
|
||||
func (s *HTTP) servRequest(req *request, c *proxy.Conn) {
|
||||
// Auth
|
||||
if s.user != "" && s.password != "" {
|
||||
if user, pass, ok := extractUserPass(req.auth); !ok || user != s.user || pass != s.password {
|
||||
io.WriteString(c, "HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic\r\n\r\n")
|
||||
log.F("[http] auth failed from %s, auth info: %s:%s", c.RemoteAddr(), user, pass)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.method == "CONNECT" {
|
||||
s.servHTTPS(req, c)
|
||||
return
|
||||
}
|
||||
|
||||
s.servHTTP(req, c)
|
||||
}
|
||||
|
||||
func (s *HTTP) servHTTPS(r *request, c net.Conn) {
|
||||
rc, dialer, err := s.proxy.Dial("tcp", r.uri)
|
||||
if err != nil {
|
||||
io.WriteString(c, r.proto+" 502 ERROR\r\n\r\n")
|
||||
log.F("[http] %s <-> %s [c] via %s, error in dial: %v", c.RemoteAddr(), r.uri, dialer.Addr(), err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
io.WriteString(c, "HTTP/1.1 200 Connection established\r\n\r\n")
|
||||
|
||||
log.F("[http] %s <-> %s [c] via %s", c.RemoteAddr(), r.uri, dialer.Addr())
|
||||
|
||||
if err = proxy.Relay(c, rc); err != nil {
|
||||
log.F("[http] %s <-> %s via %s, relay error: %v", c.RemoteAddr(), r.uri, dialer.Addr(), err)
|
||||
// record remote conn failure only
|
||||
if !strings.Contains(err.Error(), s.addr) {
|
||||
s.proxy.Record(dialer, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTP) servHTTP(req *request, c *proxy.Conn) {
|
||||
rc, dialer, err := s.proxy.Dial("tcp", req.target)
|
||||
if err != nil {
|
||||
fmt.Fprintf(c, "%s 502 ERROR\r\n\r\n", req.proto)
|
||||
log.F("[http] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), req.target, dialer.Addr(), err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
buf := pool.GetBytesBuffer()
|
||||
defer pool.PutBytesBuffer(buf)
|
||||
|
||||
// send request to remote server
|
||||
req.WriteBuf(buf)
|
||||
_, err = rc.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// copy the left request bytes to remote server. eg. length specificed or chunked body.
|
||||
go func() {
|
||||
if _, err := c.Reader().Peek(1); err == nil {
|
||||
proxy.Copy(rc, c)
|
||||
rc.SetDeadline(time.Now())
|
||||
c.SetDeadline(time.Now())
|
||||
}
|
||||
}()
|
||||
|
||||
r := pool.GetBufReader(rc)
|
||||
defer pool.PutBufReader(r)
|
||||
|
||||
tpr := textproto.NewReader(r)
|
||||
line, err := tpr.ReadLine()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
proto, code, status, ok := parseStartLine(line)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
header, err := tpr.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
log.F("[http] read header error:%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
header.Set("Proxy-Connection", "close")
|
||||
header.Set("Connection", "close")
|
||||
|
||||
buf.Reset()
|
||||
writeStartLine(buf, proto, code, status)
|
||||
writeHeaders(buf, header)
|
||||
|
||||
log.F("[http] %s <-> %s via %s", c.RemoteAddr(), req.target, dialer.Addr())
|
||||
c.Write(buf.Bytes())
|
||||
|
||||
proxy.Copy(c, r)
|
||||
}
|
281
proxy/kcp/kcp.go
Normal file
281
proxy/kcp/kcp.go
Normal file
@ -0,0 +1,281 @@
|
||||
package kcp
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
kcp "github.com/xtaci/kcp-go/v5"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// KCP struct.
|
||||
type KCP struct {
|
||||
dialer proxy.Dialer
|
||||
proxy proxy.Proxy
|
||||
addr string
|
||||
|
||||
key string
|
||||
crypt string
|
||||
block kcp.BlockCrypt
|
||||
mode string
|
||||
|
||||
dataShards int
|
||||
parityShards int
|
||||
|
||||
server proxy.Server
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.RegisterDialer("kcp", NewKCPDialer)
|
||||
proxy.RegisterServer("kcp", NewKCPServer)
|
||||
}
|
||||
|
||||
// NewKCP returns a kcp proxy struct.
|
||||
func NewKCP(s string, d proxy.Dialer, p proxy.Proxy) (*KCP, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
log.F("[kcp] parse url err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := u.Host
|
||||
crypt := u.User.Username()
|
||||
key, _ := u.User.Password()
|
||||
|
||||
query := u.Query()
|
||||
|
||||
// dataShards
|
||||
dShards := query.Get("dataShards")
|
||||
if dShards == "" {
|
||||
dShards = "10"
|
||||
}
|
||||
|
||||
dataShards, err := strconv.ParseUint(dShards, 10, 32)
|
||||
if err != nil {
|
||||
log.F("[kcp] parse dataShards err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parityShards
|
||||
pShards := query.Get("parityShards")
|
||||
if pShards == "" {
|
||||
pShards = "3"
|
||||
}
|
||||
|
||||
parityShards, err := strconv.ParseUint(pShards, 10, 32)
|
||||
if err != nil {
|
||||
log.F("[kcp] parse parityShards err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k := &KCP{
|
||||
dialer: d,
|
||||
proxy: p,
|
||||
addr: addr,
|
||||
key: key,
|
||||
crypt: crypt,
|
||||
mode: query.Get("mode"),
|
||||
dataShards: int(dataShards),
|
||||
parityShards: int(parityShards),
|
||||
}
|
||||
|
||||
if k.crypt != "" {
|
||||
k.block, err = block(k.crypt, k.key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[kcp] error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if k.mode == "" {
|
||||
k.mode = "fast"
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func block(crypt, key string) (block kcp.BlockCrypt, err error) {
|
||||
pass := pbkdf2.Key([]byte(key), []byte("kcp-go"), 4096, 32, sha1.New)
|
||||
switch crypt {
|
||||
case "sm4":
|
||||
block, _ = kcp.NewSM4BlockCrypt(pass[:16])
|
||||
case "tea":
|
||||
block, _ = kcp.NewTEABlockCrypt(pass[:16])
|
||||
case "xor":
|
||||
block, _ = kcp.NewSimpleXORBlockCrypt(pass)
|
||||
case "none":
|
||||
block, _ = kcp.NewNoneBlockCrypt(pass)
|
||||
case "aes":
|
||||
block, _ = kcp.NewAESBlockCrypt(pass)
|
||||
case "aes-128":
|
||||
block, _ = kcp.NewAESBlockCrypt(pass[:16])
|
||||
case "aes-192":
|
||||
block, _ = kcp.NewAESBlockCrypt(pass[:24])
|
||||
case "blowfish":
|
||||
block, _ = kcp.NewBlowfishBlockCrypt(pass)
|
||||
case "twofish":
|
||||
block, _ = kcp.NewTwofishBlockCrypt(pass)
|
||||
case "cast5":
|
||||
block, _ = kcp.NewCast5BlockCrypt(pass[:16])
|
||||
case "3des":
|
||||
block, _ = kcp.NewTripleDESBlockCrypt(pass[:24])
|
||||
case "xtea":
|
||||
block, _ = kcp.NewXTEABlockCrypt(pass[:16])
|
||||
case "salsa20":
|
||||
block, _ = kcp.NewSalsa20BlockCrypt(pass)
|
||||
default:
|
||||
err = errors.New("unknown crypt type '" + crypt + "'")
|
||||
}
|
||||
return block, err
|
||||
}
|
||||
|
||||
// NewKCPDialer returns a kcp proxy dialer.
|
||||
func NewKCPDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
|
||||
return NewKCP(s, d, nil)
|
||||
}
|
||||
|
||||
// NewKCPServer returns a kcp proxy server.
|
||||
func NewKCPServer(s string, p proxy.Proxy) (proxy.Server, error) {
|
||||
schemes := strings.SplitN(s, ",", 2)
|
||||
k, err := NewKCP(schemes[0], nil, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(schemes) > 1 {
|
||||
k.server, err = proxy.ServerFromURL(schemes[1], p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// ListenAndServe listens on server's addr and serves connections.
|
||||
func (s *KCP) ListenAndServe() {
|
||||
l, err := kcp.ListenWithOptions(s.addr, s.block, s.dataShards, s.parityShards)
|
||||
if err != nil {
|
||||
log.Fatalf("[kcp] failed to listen on %s: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
log.F("[kcp] listening on %s", s.addr)
|
||||
|
||||
for {
|
||||
c, err := l.AcceptKCP()
|
||||
if err != nil {
|
||||
log.F("[kcp] failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
s.setParams(c)
|
||||
|
||||
go s.Serve(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve serves connections.
|
||||
func (s *KCP) Serve(c net.Conn) {
|
||||
if s.server != nil {
|
||||
s.server.Serve(c)
|
||||
return
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
|
||||
rc, dialer, err := s.proxy.Dial("tcp", "")
|
||||
if err != nil {
|
||||
log.F("[kcp] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), s.addr, dialer.Addr(), err)
|
||||
s.proxy.Record(dialer, false)
|
||||
return
|
||||
}
|
||||
|
||||
defer rc.Close()
|
||||
|
||||
log.F("[kcp] %s <-> %s", c.RemoteAddr(), dialer.Addr())
|
||||
|
||||
if err = proxy.Relay(c, rc); err != nil {
|
||||
log.F("[kcp] %s <-> %s, relay error: %v", c.RemoteAddr(), dialer.Addr(), err)
|
||||
// record remote conn failure only
|
||||
if !strings.Contains(err.Error(), s.addr) {
|
||||
s.proxy.Record(dialer, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Addr returns forwarder's address.
|
||||
func (s *KCP) Addr() string {
|
||||
if s.addr == "" {
|
||||
return s.dialer.Addr()
|
||||
}
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the network net via the proxy.
|
||||
func (s *KCP) Dial(network, addr string) (net.Conn, error) {
|
||||
// NOTE: kcp uses udp, we should dial remote server directly here
|
||||
c, err := kcp.DialWithOptions(s.addr, s.block, s.dataShards, s.parityShards)
|
||||
if err != nil {
|
||||
log.F("[kcp] dial to %s error: %s", s.addr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.setParams(c)
|
||||
|
||||
c.SetDSCP(0)
|
||||
c.SetReadBuffer(4194304)
|
||||
c.SetWriteBuffer(4194304)
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DialUDP connects to the given address via the proxy.
|
||||
func (s *KCP) DialUDP(network, addr string) (net.PacketConn, error) {
|
||||
return nil, proxy.ErrNotSupported
|
||||
}
|
||||
|
||||
func (s *KCP) setParams(c *kcp.UDPSession) {
|
||||
// TODO: change them to customizable later?
|
||||
c.SetStreamMode(true)
|
||||
c.SetWriteDelay(false)
|
||||
|
||||
switch s.mode {
|
||||
case "normal":
|
||||
c.SetNoDelay(0, 40, 2, 1)
|
||||
case "fast":
|
||||
c.SetNoDelay(0, 30, 2, 1)
|
||||
case "fast2":
|
||||
c.SetNoDelay(1, 20, 2, 1)
|
||||
case "fast3":
|
||||
c.SetNoDelay(1, 10, 2, 1)
|
||||
default:
|
||||
log.F("[kcp] unkonw mode: %s, use fast mode instead", s.mode)
|
||||
c.SetNoDelay(0, 30, 2, 1)
|
||||
}
|
||||
|
||||
c.SetWindowSize(1024, 1024)
|
||||
c.SetMtu(1350)
|
||||
c.SetACKNoDelay(true)
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.AddUsage("kcp", `
|
||||
KCP scheme:
|
||||
kcp://CRYPT:KEY@host:port[?dataShards=NUM&parityShards=NUM&mode=MODE]
|
||||
|
||||
Available crypt types for KCP:
|
||||
none, sm4, tea, xor, aes, aes-128, aes-192, blowfish, twofish, cast5, 3des, xtea, salsa20
|
||||
|
||||
Available modes for KCP:
|
||||
fast, fast2, fast3, normal, default: fast
|
||||
`)
|
||||
}
|
90
proxy/mixed/mixed.go
Normal file
90
proxy/mixed/mixed.go
Normal file
@ -0,0 +1,90 @@
|
||||
package mixed
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
"github.com/nadoo/glider/proxy/http"
|
||||
"github.com/nadoo/glider/proxy/socks5"
|
||||
)
|
||||
|
||||
// Mixed struct.
|
||||
type Mixed struct {
|
||||
proxy proxy.Proxy
|
||||
addr string
|
||||
|
||||
httpServer *http.HTTP
|
||||
socks5Server *socks5.Socks5
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.RegisterServer("mixed", NewMixedServer)
|
||||
}
|
||||
|
||||
// NewMixed returns a mixed proxy.
|
||||
func NewMixed(s string, p proxy.Proxy) (*Mixed, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
log.F("parse err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &Mixed{
|
||||
proxy: p,
|
||||
addr: u.Host,
|
||||
}
|
||||
|
||||
m.httpServer, err = http.NewHTTP(s, nil, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.socks5Server, err = socks5.NewSocks5(s, nil, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// NewMixedServer returns a mixed server.
|
||||
func NewMixedServer(s string, p proxy.Proxy) (proxy.Server, error) {
|
||||
return NewMixed(s, p)
|
||||
}
|
||||
|
||||
// ListenAndServe listens on server's addr and serves connections.
|
||||
func (m *Mixed) ListenAndServe() {
|
||||
go m.socks5Server.ListenAndServeUDP()
|
||||
|
||||
l, err := net.Listen("tcp", m.addr)
|
||||
if err != nil {
|
||||
log.Fatalf("[mixed] failed to listen on %s: %v", m.addr, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.F("[mixed] http & socks5 server listening TCP on %s", m.addr)
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
log.F("[mixed] failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go m.Serve(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve serves connections.
|
||||
func (m *Mixed) Serve(c net.Conn) {
|
||||
conn := proxy.NewConn(c)
|
||||
if head, err := conn.Peek(1); err == nil {
|
||||
if head[0] == socks5.Version {
|
||||
m.socks5Server.Serve(conn)
|
||||
return
|
||||
}
|
||||
}
|
||||
m.httpServer.Serve(conn)
|
||||
}
|
82
proxy/obfs/http.go
Normal file
82
proxy/obfs/http.go
Normal file
@ -0,0 +1,82 @@
|
||||
package obfs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
)
|
||||
|
||||
// HTTPObfs struct
|
||||
type HTTPObfs struct {
|
||||
obfsHost string
|
||||
obfsURI string
|
||||
obfsUA string
|
||||
}
|
||||
|
||||
// NewHTTPObfs returns a HTTPObfs object
|
||||
func NewHTTPObfs(obfsHost, obfsURI, obfsUA string) *HTTPObfs {
|
||||
return &HTTPObfs{obfsHost, obfsURI, obfsUA}
|
||||
}
|
||||
|
||||
// HTTPObfsConn struct
|
||||
type HTTPObfsConn struct {
|
||||
*HTTPObfs
|
||||
|
||||
net.Conn
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
// NewConn returns a new obfs connection
|
||||
func (p *HTTPObfs) NewConn(c net.Conn) (net.Conn, error) {
|
||||
cc := &HTTPObfsConn{
|
||||
Conn: c,
|
||||
HTTPObfs: p,
|
||||
}
|
||||
|
||||
// send http header to remote server
|
||||
_, err := cc.writeHeader()
|
||||
return cc, err
|
||||
}
|
||||
|
||||
func (c *HTTPObfsConn) writeHeader() (int, error) {
|
||||
buf := pool.GetBytesBuffer()
|
||||
defer pool.PutBytesBuffer(buf)
|
||||
|
||||
buf.WriteString("GET " + c.obfsURI + " HTTP/1.1\r\n")
|
||||
buf.WriteString("Host: " + c.obfsHost + "\r\n")
|
||||
buf.WriteString("User-Agent: " + c.obfsUA + "\r\n")
|
||||
buf.WriteString("Upgrade: websocket\r\n")
|
||||
buf.WriteString("Connection: Upgrade\r\n")
|
||||
|
||||
b := pool.GetBuffer(16)
|
||||
rand.Read(b)
|
||||
buf.WriteString("Sec-WebSocket-Key: " + base64.StdEncoding.EncodeToString(b) + "\r\n")
|
||||
pool.PutBuffer(b)
|
||||
|
||||
buf.WriteString("\r\n")
|
||||
|
||||
return c.Conn.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
func (c *HTTPObfsConn) Read(b []byte) (n int, err error) {
|
||||
if c.reader == nil {
|
||||
r := bufio.NewReader(c.Conn)
|
||||
c.reader = r
|
||||
for {
|
||||
l, _, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(l) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.reader.Read(b)
|
||||
}
|
121
proxy/obfs/obfs.go
Normal file
121
proxy/obfs/obfs.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Package obfs implements simple-obfs of ss
|
||||
package obfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// Obfs struct.
|
||||
type Obfs struct {
|
||||
dialer proxy.Dialer
|
||||
addr string
|
||||
|
||||
obfsType string
|
||||
obfsHost string
|
||||
obfsURI string
|
||||
obfsUA string
|
||||
|
||||
obfsConn func(c net.Conn) (net.Conn, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.RegisterDialer("simple-obfs", NewObfsDialer)
|
||||
}
|
||||
|
||||
// NewObfs returns a proxy struct.
|
||||
func NewObfs(s string, d proxy.Dialer) (*Obfs, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
log.F("parse url err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := u.Host
|
||||
|
||||
query := u.Query()
|
||||
obfsType := query.Get("type")
|
||||
if obfsType == "" {
|
||||
obfsType = "http"
|
||||
}
|
||||
|
||||
obfsHost := query.Get("host")
|
||||
if obfsHost == "" {
|
||||
return nil, errors.New("[obfs] host cannot be null")
|
||||
}
|
||||
|
||||
obfsURI := query.Get("uri")
|
||||
if obfsURI == "" {
|
||||
obfsURI = "/"
|
||||
}
|
||||
|
||||
obfsUA := query.Get("ua")
|
||||
if obfsUA == "" {
|
||||
obfsUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36"
|
||||
}
|
||||
|
||||
p := &Obfs{
|
||||
dialer: d,
|
||||
addr: addr,
|
||||
obfsType: obfsType,
|
||||
obfsHost: obfsHost,
|
||||
obfsURI: obfsURI,
|
||||
obfsUA: obfsUA,
|
||||
}
|
||||
|
||||
switch obfsType {
|
||||
case "http":
|
||||
httpObfs := NewHTTPObfs(obfsHost, obfsURI, obfsUA)
|
||||
p.obfsConn = httpObfs.NewConn
|
||||
case "tls":
|
||||
tlsObfs := NewTLSObfs(obfsHost)
|
||||
p.obfsConn = tlsObfs.NewConn
|
||||
default:
|
||||
return nil, errors.New("[obfs] unknown obfs type: " + obfsType)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// NewObfsDialer returns a proxy dialer.
|
||||
func NewObfsDialer(s string, dialer proxy.Dialer) (proxy.Dialer, error) {
|
||||
return NewObfs(s, dialer)
|
||||
}
|
||||
|
||||
// Addr returns forwarder's address.
|
||||
func (s *Obfs) Addr() string {
|
||||
if s.addr == "" {
|
||||
return s.dialer.Addr()
|
||||
}
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the network net via the proxy.
|
||||
func (s *Obfs) Dial(network, addr string) (net.Conn, error) {
|
||||
c, err := s.dialer.Dial("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.F("[obfs] dial to %s error: %s", s.addr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.obfsConn(c)
|
||||
}
|
||||
|
||||
// DialUDP connects to the given address via the proxy.
|
||||
func (s *Obfs) DialUDP(network, addr string) (net.PacketConn, error) {
|
||||
return nil, proxy.ErrNotSupported
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.AddUsage("simple-obfs", `
|
||||
Simple-Obfs scheme:
|
||||
simple-obfs://host:port[?type=TYPE&host=HOST&uri=URI&ua=UA]
|
||||
|
||||
Available types for simple-obfs:
|
||||
http, tls
|
||||
`)
|
||||
}
|
265
proxy/obfs/tls.go
Normal file
265
proxy/obfs/tls.go
Normal file
@ -0,0 +1,265 @@
|
||||
// https://www.rfc-editor.org/rfc/rfc5246
|
||||
// https://golang.org/src/crypto/tls/handshake_messages.go
|
||||
|
||||
// NOTE:
|
||||
// The official obfs-server only checks 6 static bytes of client hello packet,
|
||||
// so if we send a malformed packet, e.g: set a wrong length number of extensions,
|
||||
// obfs-server will treat it as a correct packet, but in wireshak, it's malformed.
|
||||
|
||||
package obfs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
lenSize = 2
|
||||
chunkSize = 1 << 13 // 8192
|
||||
)
|
||||
|
||||
// TLSObfs struct
|
||||
type TLSObfs struct {
|
||||
obfsHost string
|
||||
}
|
||||
|
||||
// NewTLSObfs returns a TLSObfs object
|
||||
func NewTLSObfs(obfsHost string) *TLSObfs {
|
||||
return &TLSObfs{obfsHost: obfsHost}
|
||||
}
|
||||
|
||||
// TLSObfsConn struct
|
||||
type TLSObfsConn struct {
|
||||
*TLSObfs
|
||||
|
||||
net.Conn
|
||||
reqSent bool
|
||||
reader *bufio.Reader
|
||||
buf [lenSize]byte
|
||||
leftBytes int
|
||||
}
|
||||
|
||||
// NewConn returns a new obfs connection
|
||||
func (p *TLSObfs) NewConn(c net.Conn) (net.Conn, error) {
|
||||
cc := &TLSObfsConn{Conn: c, TLSObfs: p}
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
func (c *TLSObfsConn) Write(b []byte) (int, error) {
|
||||
if !c.reqSent {
|
||||
c.reqSent = true
|
||||
return c.handshake(b)
|
||||
}
|
||||
|
||||
buf := pool.GetBytesBuffer()
|
||||
defer pool.PutBytesBuffer(buf)
|
||||
|
||||
n := len(b)
|
||||
for i := 0; i < n; i += chunkSize {
|
||||
buf.Reset()
|
||||
end := min(i+chunkSize, n)
|
||||
|
||||
buf.Write([]byte{0x17, 0x03, 0x03})
|
||||
binary.Write(buf, binary.BigEndian, uint16(len(b[i:end])))
|
||||
buf.Write(b[i:end])
|
||||
|
||||
_, err := c.Conn.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *TLSObfsConn) Read(b []byte) (int, error) {
|
||||
if c.reader == nil {
|
||||
c.reader = bufio.NewReader(c.Conn)
|
||||
// Server Hello
|
||||
// TLSv1.2 Record Layer: Handshake Protocol: Server Hello (96 bytes)
|
||||
// TLSv1.2 Record Layer: Change Cipher Spec Protocol: Change Cipher Spec (6 bytes)
|
||||
c.reader.Discard(102)
|
||||
}
|
||||
|
||||
if c.leftBytes == 0 {
|
||||
// TLSv1.2 Record Layer:
|
||||
// 1st packet: handshake encrypted message / following packets: application data
|
||||
// 1 byte: Content Type: Handshake (22) / Application Data (23)
|
||||
// 2 bytes: Version: TLS 1.2 (0x0303)
|
||||
c.reader.Discard(3)
|
||||
|
||||
// get length
|
||||
_, err := io.ReadFull(c.reader, c.buf[:lenSize])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
c.leftBytes = int(binary.BigEndian.Uint16(c.buf[:lenSize]))
|
||||
}
|
||||
|
||||
readLen := len(b)
|
||||
if readLen > c.leftBytes {
|
||||
readLen = c.leftBytes
|
||||
}
|
||||
|
||||
m, err := c.reader.Read(b[:readLen])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
c.leftBytes -= m
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (c *TLSObfsConn) handshake(b []byte) (int, error) {
|
||||
buf := pool.GetBytesBuffer()
|
||||
defer pool.PutBytesBuffer(buf)
|
||||
|
||||
bufExt := pool.GetBytesBuffer()
|
||||
defer pool.PutBytesBuffer(bufExt)
|
||||
|
||||
bufHello := pool.GetBytesBuffer()
|
||||
defer pool.PutBytesBuffer(bufHello)
|
||||
|
||||
// prepare extension & clientHello content
|
||||
extension(b, c.obfsHost, bufExt)
|
||||
clientHello(bufHello)
|
||||
|
||||
// prepare lengths
|
||||
extLen := bufExt.Len()
|
||||
helloLen := bufHello.Len() + 2 + extLen // 2: len(extContentLength)
|
||||
handshakeLen := 4 + helloLen // 1: len(0x01) + 3: len(clientHelloContentLength)
|
||||
|
||||
// TLS Record Layer Begin
|
||||
// Content Type: Handshake (22)
|
||||
buf.WriteByte(0x16)
|
||||
|
||||
// Version: TLS 1.0 (0x0301)
|
||||
buf.Write([]byte{0x03, 0x01})
|
||||
|
||||
// length
|
||||
binary.Write(buf, binary.BigEndian, uint16(handshakeLen))
|
||||
|
||||
// Handshake Begin
|
||||
// Handshake Type: Client Hello (1)
|
||||
buf.WriteByte(0x01)
|
||||
|
||||
// length: uint24(3 bytes), but golang doesn't have this type
|
||||
buf.Write([]byte{uint8(helloLen >> 16), uint8(helloLen >> 8), uint8(helloLen)})
|
||||
|
||||
// clientHello content
|
||||
buf.Write(bufHello.Bytes())
|
||||
|
||||
// Extension Begin
|
||||
// ext content length
|
||||
binary.Write(buf, binary.BigEndian, uint16(extLen))
|
||||
|
||||
// ext content
|
||||
buf.Write(bufExt.Bytes())
|
||||
|
||||
_, err := c.Conn.Write(buf.Bytes())
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func clientHello(buf *bytes.Buffer) {
|
||||
// Version: TLS 1.2 (0x0303)
|
||||
buf.Write([]byte{0x03, 0x03})
|
||||
|
||||
// Random
|
||||
// https://tools.ietf.org/id/draft-mathewson-no-gmtunixtime-00.txt
|
||||
// NOTE:
|
||||
// Most tls implementations do not deal with the first 4 bytes unix time,
|
||||
// clients do not send current time, and server do not check it,
|
||||
// golang tls client and chrome browser send random bytes instead.
|
||||
//
|
||||
binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix()))
|
||||
random := make([]byte, 28)
|
||||
// The above 2 lines of codes was added to make it compatible with some server implementation,
|
||||
// if we don't need the compatibility, just use the following code instead.
|
||||
// random := make([]byte, 32)
|
||||
|
||||
rand.Read(random)
|
||||
buf.Write(random)
|
||||
|
||||
// Session ID Length: 32
|
||||
buf.WriteByte(32)
|
||||
// Session ID
|
||||
sessionID := make([]byte, 32)
|
||||
rand.Read(sessionID)
|
||||
buf.Write(sessionID)
|
||||
|
||||
// https://github.com/shadowsocks/simple-obfs/blob/7659eeccf473aa41eb294e92c32f8f60a8747325/src/obfs_tls.c#L57
|
||||
// Cipher Suites Length: 56
|
||||
binary.Write(buf, binary.BigEndian, uint16(56))
|
||||
// Cipher Suites (28 suites)
|
||||
buf.Write([]byte{
|
||||
0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f,
|
||||
0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a,
|
||||
0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d,
|
||||
0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff,
|
||||
})
|
||||
|
||||
// Compression Methods Length: 1
|
||||
buf.WriteByte(0x01)
|
||||
// Compression Methods (1 method)
|
||||
buf.WriteByte(0x00)
|
||||
}
|
||||
|
||||
func extension(b []byte, server string, buf *bytes.Buffer) {
|
||||
// Extension: SessionTicket TLS
|
||||
buf.Write([]byte{0x00, 0x23}) // type
|
||||
// NOTE: send some data in sessionticket, the server will treat it as data too
|
||||
binary.Write(buf, binary.BigEndian, uint16(len(b))) // length
|
||||
buf.Write(b)
|
||||
|
||||
// Extension: server_name
|
||||
buf.Write([]byte{0x00, 0x00}) // type
|
||||
binary.Write(buf, binary.BigEndian, uint16(len(server)+5)) // length
|
||||
binary.Write(buf, binary.BigEndian, uint16(len(server)+3)) // Server Name list length
|
||||
buf.WriteByte(0x00) // Server Name Type: host_name (0)
|
||||
binary.Write(buf, binary.BigEndian, uint16(len(server))) // Server Name length
|
||||
buf.WriteString(server)
|
||||
|
||||
// https://github.com/shadowsocks/simple-obfs/blob/7659eeccf473aa41eb294e92c32f8f60a8747325/src/obfs_tls.c#L88
|
||||
// Extension: ec_point_formats (len=4)
|
||||
buf.Write([]byte{0x00, 0x0b}) // type
|
||||
binary.Write(buf, binary.BigEndian, uint16(4)) // length
|
||||
buf.WriteByte(0x03) // format length
|
||||
buf.Write([]byte{0x01, 0x00, 0x02})
|
||||
|
||||
// Extension: supported_groups (len=10)
|
||||
buf.Write([]byte{0x00, 0x0a}) // type
|
||||
binary.Write(buf, binary.BigEndian, uint16(10)) // length
|
||||
binary.Write(buf, binary.BigEndian, uint16(8)) // Supported Groups List Length: 8
|
||||
buf.Write([]byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18})
|
||||
|
||||
// Extension: signature_algorithms (len=32)
|
||||
buf.Write([]byte{0x00, 0x0d}) // type
|
||||
binary.Write(buf, binary.BigEndian, uint16(32)) // length
|
||||
binary.Write(buf, binary.BigEndian, uint16(30)) // Signature Hash Algorithms Length: 30
|
||||
buf.Write([]byte{
|
||||
0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02,
|
||||
0x04, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03,
|
||||
})
|
||||
|
||||
// Extension: encrypt_then_mac (len=0)
|
||||
buf.Write([]byte{0x00, 0x16}) // type
|
||||
binary.Write(buf, binary.BigEndian, uint16(0)) // length
|
||||
|
||||
// Extension: extended_master_secret (len=0)
|
||||
buf.Write([]byte{0x00, 0x17}) // type
|
||||
binary.Write(buf, binary.BigEndian, uint16(0)) // length
|
||||
}
|
46
proxy/proxy.go
Normal file
46
proxy/proxy.go
Normal file
@ -0,0 +1,46 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Proxy is a dialer manager.
|
||||
type Proxy interface {
|
||||
// Dial connects to the given address via the proxy.
|
||||
Dial(network, addr string) (c net.Conn, dialer Dialer, err error)
|
||||
|
||||
// DialUDP connects to the given address via the proxy.
|
||||
DialUDP(network, addr string) (pc net.PacketConn, dialer UDPDialer, err error)
|
||||
|
||||
// Get the dialer by dstAddr.
|
||||
NextDialer(dstAddr string) Dialer
|
||||
|
||||
// Record records result while using the dialer from proxy.
|
||||
Record(dialer Dialer, success bool)
|
||||
}
|
||||
|
||||
var (
|
||||
msg strings.Builder
|
||||
usages = make(map[string]string)
|
||||
)
|
||||
|
||||
// AddUsage adds help message for the named proxy.
|
||||
func AddUsage(name, usage string) {
|
||||
usages[name] = usage
|
||||
msg.WriteString(usage)
|
||||
msg.WriteString("\n--")
|
||||
}
|
||||
|
||||
// Usage returns help message of the named proxy.
|
||||
func Usage(name string) string {
|
||||
if name == "all" {
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
if usage, ok := usages[name]; ok {
|
||||
return usage
|
||||
}
|
||||
|
||||
return "can not find usage for: " + name
|
||||
}
|
134
proxy/pxyproto/server.go
Normal file
134
proxy/pxyproto/server.go
Normal file
@ -0,0 +1,134 @@
|
||||
package pxyproto
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
proxy.RegisterServer("pxyproto", NewPxyProtoServer)
|
||||
}
|
||||
|
||||
// PxyProtoServer struct.
|
||||
type PxyProtoServer struct {
|
||||
addr string
|
||||
proxy proxy.Proxy
|
||||
server proxy.Server
|
||||
}
|
||||
|
||||
// NewPxyProtoServer returns a PxyProtoServer struct.
|
||||
func NewPxyProtoServer(s string, p proxy.Proxy) (proxy.Server, error) {
|
||||
schemes := strings.SplitN(s, ",", 2)
|
||||
u, err := url.Parse(schemes[0])
|
||||
if err != nil {
|
||||
log.F("[pxyproto] parse url err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &PxyProtoServer{proxy: p, addr: u.Host}
|
||||
if len(schemes) < 2 {
|
||||
return nil, errors.New("[pxyproto] you must use pxyproto with a proxy server, e.g: pxyproto://:1234,http://")
|
||||
}
|
||||
|
||||
t.server, err = proxy.ServerFromURL(schemes[1], p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ListenAndServe listens on server's addr and serves connections.
|
||||
func (s *PxyProtoServer) ListenAndServe() {
|
||||
l, err := net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.Fatalf("[pxyproto] failed to listen on %s: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
log.F("[pxyproto] listening TCP on %s", s.addr)
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
log.F("[pxyproto] failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go s.Serve(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve serves a connection.
|
||||
func (s *PxyProtoServer) Serve(cc net.Conn) {
|
||||
c, err := newServerConn(cc)
|
||||
if err != nil {
|
||||
log.F("[pxyproto] parse header failed, error: %v", err)
|
||||
cc.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// log.F("[pxyproto] %s <-> %s <-> %s <-> %s",
|
||||
// c.RemoteAddr(), c.LocalAddr(), cc.RemoteAddr(), cc.LocalAddr())
|
||||
|
||||
if s.server != nil {
|
||||
s.server.Serve(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type serverConn struct {
|
||||
*proxy.Conn
|
||||
src, dst net.Addr
|
||||
}
|
||||
|
||||
func newServerConn(c net.Conn) (*serverConn, error) {
|
||||
sc := &serverConn{
|
||||
Conn: proxy.NewConn(c),
|
||||
src: c.RemoteAddr(),
|
||||
dst: c.LocalAddr(),
|
||||
}
|
||||
return sc, sc.parseHeader()
|
||||
}
|
||||
|
||||
// "PROXY TCPx SRC_IP DST_IP SRC_PORT DST_PORT"
|
||||
func (c *serverConn) parseHeader() error {
|
||||
line, err := c.Conn.Reader().ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
line = strings.ReplaceAll(line, "\r\n", "")
|
||||
// log.F("[pxyproto] req header: %s", line)
|
||||
|
||||
header := strings.Split(line, " ")
|
||||
if len(header) != 6 {
|
||||
return fmt.Errorf("invalid header: %s", line)
|
||||
}
|
||||
|
||||
if header[0] != "PROXY" {
|
||||
return fmt.Errorf("invalid header: %s", line)
|
||||
}
|
||||
|
||||
c.src, err = net.ResolveTCPAddr("tcp", net.JoinHostPort(header[2], header[4]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse header: %s, error: %v", line, err)
|
||||
}
|
||||
|
||||
c.dst, err = net.ResolveTCPAddr("tcp", net.JoinHostPort(header[3], header[5]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse header: %s, error: %v", line, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverConn) LocalAddr() net.Addr { return c.dst }
|
||||
func (c *serverConn) RemoteAddr() net.Addr { return c.src }
|
161
proxy/redir/redir_linux.go
Normal file
161
proxy/redir/redir_linux.go
Normal file
@ -0,0 +1,161 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// RedirProxy struct.
|
||||
type RedirProxy struct {
|
||||
proxy proxy.Proxy
|
||||
addr string
|
||||
ipv6 bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.RegisterServer("redir", NewRedirServer)
|
||||
proxy.RegisterServer("redir6", NewRedir6Server)
|
||||
}
|
||||
|
||||
// NewRedirProxy returns a redirect proxy.
|
||||
func NewRedirProxy(s string, p proxy.Proxy, ipv6 bool) (*RedirProxy, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
log.F("parse err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := u.Host
|
||||
r := &RedirProxy{
|
||||
proxy: p,
|
||||
addr: addr,
|
||||
ipv6: ipv6,
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// NewRedirServer returns a redir server.
|
||||
func NewRedirServer(s string, p proxy.Proxy) (proxy.Server, error) {
|
||||
return NewRedirProxy(s, p, false)
|
||||
}
|
||||
|
||||
// NewRedir6Server returns a redir server for ipv6.
|
||||
func NewRedir6Server(s string, p proxy.Proxy) (proxy.Server, error) {
|
||||
return NewRedirProxy(s, p, true)
|
||||
}
|
||||
|
||||
// ListenAndServe listens on server's addr and serves connections.
|
||||
func (s *RedirProxy) ListenAndServe() {
|
||||
l, err := net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.Fatalf("[redir] failed to listen on %s: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.F("[redir] listening TCP on %s", s.addr)
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
log.F("[redir] failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go s.Serve(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve serves connections.
|
||||
func (s *RedirProxy) Serve(cc net.Conn) {
|
||||
defer cc.Close()
|
||||
|
||||
c, ok := cc.(*net.TCPConn)
|
||||
if !ok {
|
||||
log.F("[redir] not a tcp connection, can not chain redir proxy")
|
||||
return
|
||||
}
|
||||
|
||||
c.SetKeepAlive(true)
|
||||
tgtAddr, err := getOrigDst(c, s.ipv6)
|
||||
if err != nil {
|
||||
log.F("[redir] failed to get target address: %v", err)
|
||||
return
|
||||
}
|
||||
tgt := tgtAddr.String()
|
||||
|
||||
// loop request
|
||||
if c.LocalAddr().String() == tgt {
|
||||
log.F("[redir] %s <-> %s, unallowed request to redir port", c.RemoteAddr(), tgt)
|
||||
return
|
||||
}
|
||||
|
||||
rc, dialer, err := s.proxy.Dial("tcp", tgt)
|
||||
if err != nil {
|
||||
log.F("[redir] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
log.F("[redir] %s <-> %s via %s", c.RemoteAddr(), tgt, dialer.Addr())
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the original destination of a TCP connection.
|
||||
func getOrigDst(c *net.TCPConn, ipv6 bool) (netip.AddrPort, error) {
|
||||
rc, err := c.SyscallConn()
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, err
|
||||
}
|
||||
var addr netip.AddrPort
|
||||
rc.Control(func(fd uintptr) {
|
||||
if ipv6 {
|
||||
addr, err = getorigdstIPv6(fd)
|
||||
} else {
|
||||
addr, err = getorigdst(fd)
|
||||
}
|
||||
})
|
||||
return addr, err
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
// NOTE: raw.Port is big-endian, just change it to little-endian
|
||||
// TODO: improve here when we add big-endian $GOARCH support
|
||||
port := raw.Port<<8 | raw.Port>>8
|
||||
return netip.AddrPortFrom(netip.AddrFrom4(raw.Addr), port), nil
|
||||
}
|
||||
|
||||
// Call ipv6_getorigdst() from linux/net/ipv6/netfilter/nf_conntrack_l3proto_ipv6.c
|
||||
func getorigdstIPv6(fd uintptr) (netip.AddrPort, error) {
|
||||
const _IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h
|
||||
var raw syscall.RawSockaddrInet6
|
||||
siz := unsafe.Sizeof(raw)
|
||||
if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IPV6, _IP6T_SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil {
|
||||
return netip.AddrPort{}, 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package redir
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// https://github.com/golang/go/blob/9e6b79a5dfb2f6fe4301ced956419a0da83bd025/src/syscall/syscall_linux_386.go#L196
|
||||
const GETSOCKOPT = 15
|
||||
const GETSOCKOPT = 15 // https://golang.org/src/syscall/syscall_linux_386.go#L183
|
||||
|
||||
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
|
||||
var a [6]uintptr
|
@ -1,6 +1,6 @@
|
||||
// +build linux,!386
|
||||
//go:build linux && !386
|
||||
|
||||
package main
|
||||
package redir
|
||||
|
||||
import "syscall"
|
||||
|
46
proxy/reject/reject.go
Normal file
46
proxy/reject/reject.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Package reject implements a virtual proxy which always reject requests.
|
||||
package reject
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// A Reject represents the base struct of a reject proxy.
|
||||
type Reject struct{}
|
||||
|
||||
func init() {
|
||||
proxy.RegisterDialer("reject", NewRejectDialer)
|
||||
}
|
||||
|
||||
// NewReject returns a reject proxy, reject://.
|
||||
func NewReject(s string, d proxy.Dialer) (*Reject, error) {
|
||||
return &Reject{}, nil
|
||||
}
|
||||
|
||||
// NewRejectDialer returns a reject proxy dialer.
|
||||
func NewRejectDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
|
||||
return NewReject(s, d)
|
||||
}
|
||||
|
||||
// Addr returns forwarder's address.
|
||||
func (s *Reject) Addr() string { return "REJECT" }
|
||||
|
||||
// Dial connects to the address addr on the network net via the proxy.
|
||||
func (s *Reject) Dial(network, addr string) (net.Conn, error) {
|
||||
return nil, errors.New("REJECT")
|
||||
}
|
||||
|
||||
// DialUDP connects to the given address via the proxy.
|
||||
func (s *Reject) DialUDP(network, addr string) (net.PacketConn, error) {
|
||||
return nil, errors.New("REJECT")
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.AddUsage("reject", `
|
||||
Reject scheme:
|
||||
reject://
|
||||
`)
|
||||
}
|
64
proxy/server.go
Normal file
64
proxy/server.go
Normal file
@ -0,0 +1,64 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Server interface.
|
||||
type Server interface {
|
||||
// ListenAndServe sets up a listener and serve on it
|
||||
ListenAndServe()
|
||||
|
||||
// Serve serves a connection
|
||||
Serve(c net.Conn)
|
||||
}
|
||||
|
||||
// PacketServer interface.
|
||||
type PacketServer interface {
|
||||
ServePacket(pc net.PacketConn)
|
||||
}
|
||||
|
||||
// ServerCreator is a function to create proxy servers.
|
||||
type ServerCreator func(s string, proxy Proxy) (Server, error)
|
||||
|
||||
var (
|
||||
serverCreators = make(map[string]ServerCreator)
|
||||
)
|
||||
|
||||
// RegisterServer is used to register a proxy server.
|
||||
func RegisterServer(name string, c ServerCreator) {
|
||||
serverCreators[strings.ToLower(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 {
|
||||
return nil, errors.New("ServerFromURL: dialer cannot be nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(s, "://") {
|
||||
s = "mixed://" + s
|
||||
}
|
||||
|
||||
scheme := s[:strings.Index(s, ":")]
|
||||
c, ok := serverCreators[strings.ToLower(scheme)]
|
||||
if ok {
|
||||
return c(s, proxy)
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown scheme '" + scheme + "'")
|
||||
}
|
||||
|
||||
// ServerSchemes returns the registered server schemes.
|
||||
func ServerSchemes() string {
|
||||
s := make([]string, 0, len(serverCreators))
|
||||
for name := range serverCreators {
|
||||
s = append(s, name)
|
||||
}
|
||||
sort.Strings(s)
|
||||
return strings.Join(s, " ")
|
||||
}
|
79
proxy/smux/client.go
Normal file
79
proxy/smux/client.go
Normal file
@ -0,0 +1,79 @@
|
||||
package smux
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/pkg/smux"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// SmuxClient struct.
|
||||
type SmuxClient struct {
|
||||
dialer proxy.Dialer
|
||||
addr string
|
||||
mu sync.Mutex
|
||||
session *smux.Session
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.RegisterDialer("smux", NewSmuxDialer)
|
||||
}
|
||||
|
||||
// NewSmuxDialer returns a smux dialer.
|
||||
func NewSmuxDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
log.F("[smux] parse url err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &SmuxClient{
|
||||
dialer: d,
|
||||
addr: u.Host,
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Addr returns forwarder's address.
|
||||
func (s *SmuxClient) Addr() string {
|
||||
if s.addr == "" {
|
||||
return s.dialer.Addr()
|
||||
}
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the network net via the proxy.
|
||||
func (s *SmuxClient) Dial(network, addr string) (net.Conn, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.session != nil {
|
||||
if c, err := s.session.OpenStream(); err == nil {
|
||||
return c, err
|
||||
}
|
||||
s.session.Close()
|
||||
}
|
||||
if err := s.initConn(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.session.OpenStream()
|
||||
}
|
||||
|
||||
// DialUDP connects to the given address via the proxy.
|
||||
func (s *SmuxClient) DialUDP(network, addr string) (net.PacketConn, error) {
|
||||
return nil, proxy.ErrNotSupported
|
||||
}
|
||||
|
||||
func (s *SmuxClient) initConn() error {
|
||||
conn, err := s.dialer.Dial("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.F("[smux] dial to %s error: %s", s.addr, err)
|
||||
return err
|
||||
}
|
||||
s.session, err = smux.Client(conn, nil)
|
||||
return err
|
||||
}
|
119
proxy/smux/server.go
Normal file
119
proxy/smux/server.go
Normal file
@ -0,0 +1,119 @@
|
||||
package smux
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/pkg/smux"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// SmuxServer struct.
|
||||
type SmuxServer struct {
|
||||
proxy proxy.Proxy
|
||||
addr string
|
||||
server proxy.Server
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.RegisterServer("smux", NewSmuxServer)
|
||||
}
|
||||
|
||||
// NewSmuxServer returns a smux transport layer before the real server.
|
||||
func NewSmuxServer(s string, p proxy.Proxy) (proxy.Server, error) {
|
||||
schemes := strings.SplitN(s, ",", 2)
|
||||
u, err := url.Parse(schemes[0])
|
||||
if err != nil {
|
||||
log.F("[smux] parse url err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &SmuxServer{
|
||||
proxy: p,
|
||||
addr: u.Host,
|
||||
}
|
||||
|
||||
if len(schemes) > 1 {
|
||||
m.server, err = proxy.ServerFromURL(schemes[1], p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ListenAndServe listens on server's addr and serves connections.
|
||||
func (s *SmuxServer) ListenAndServe() {
|
||||
l, err := net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.Fatalf("[smux] failed to listen on %s: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
log.F("[smux] listening mux on %s", s.addr)
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
log.F("[smux] failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go s.Serve(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve serves a connection.
|
||||
func (s *SmuxServer) Serve(c net.Conn) {
|
||||
// we know the internal server will close the connection after serve
|
||||
// defer c.Close()
|
||||
|
||||
session, err := smux.Server(c, nil)
|
||||
if err != nil {
|
||||
log.F("[smux] failed to create session: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
// Accept a stream
|
||||
stream, err := session.AcceptStream()
|
||||
if err != nil {
|
||||
session.Close()
|
||||
break
|
||||
}
|
||||
go s.ServeStream(stream)
|
||||
}
|
||||
}
|
||||
|
||||
// ServeStream serves a smux stream.
|
||||
func (s *SmuxServer) ServeStream(c *smux.Stream) {
|
||||
if s.server != nil {
|
||||
s.server.Serve(c)
|
||||
return
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
|
||||
rc, dialer, err := s.proxy.Dial("tcp", "")
|
||||
if err != nil {
|
||||
log.F("[smux] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), s.addr, dialer.Addr(), err)
|
||||
s.proxy.Record(dialer, false)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
log.F("[smux] %s <-> %s", c.RemoteAddr(), dialer.Addr())
|
||||
|
||||
if err = proxy.Relay(c, rc); err != nil {
|
||||
log.F("[smux] %s <-> %s, relay error: %v", c.RemoteAddr(), dialer.Addr(), err)
|
||||
// record remote conn failure only
|
||||
if !strings.Contains(err.Error(), s.addr) {
|
||||
s.proxy.Record(dialer, false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
10
proxy/smux/smux.go
Normal file
10
proxy/smux/smux.go
Normal file
@ -0,0 +1,10 @@
|
||||
package smux
|
||||
|
||||
import "github.com/nadoo/glider/proxy"
|
||||
|
||||
func init() {
|
||||
proxy.AddUsage("smux", `
|
||||
Smux scheme:
|
||||
smux://host:port
|
||||
`)
|
||||
}
|
195
proxy/socks4/socks4.go
Normal file
195
proxy/socks4/socks4.go
Normal file
@ -0,0 +1,195 @@
|
||||
// https://www.openssh.com/txt/socks4.protocol
|
||||
|
||||
// socks4 client
|
||||
|
||||
package socks4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
const (
|
||||
// Version is socks4 version number.
|
||||
Version = 4
|
||||
// ConnectCommand connect command byte
|
||||
ConnectCommand = 1
|
||||
)
|
||||
|
||||
// SOCKS4 is a base socks4 struct.
|
||||
type SOCKS4 struct {
|
||||
dialer proxy.Dialer
|
||||
addr string
|
||||
socks4a bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.RegisterDialer("socks4", NewSocks4Dialer)
|
||||
proxy.RegisterDialer("socks4a", NewSocks4Dialer)
|
||||
}
|
||||
|
||||
// NewSOCKS4 returns a socks4 proxy.
|
||||
func NewSOCKS4(s string, dialer proxy.Dialer) (*SOCKS4, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
log.F("parse err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h := &SOCKS4{
|
||||
dialer: dialer,
|
||||
addr: u.Host,
|
||||
socks4a: u.Scheme == "socks4a",
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// NewSocks4Dialer returns a socks4 proxy dialer.
|
||||
func NewSocks4Dialer(s string, dialer proxy.Dialer) (proxy.Dialer, error) {
|
||||
return NewSOCKS4(s, dialer)
|
||||
}
|
||||
|
||||
// Addr returns forwarder's address.
|
||||
func (s *SOCKS4) Addr() string {
|
||||
if s.addr == "" {
|
||||
return s.dialer.Addr()
|
||||
}
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the network net via the SOCKS4 proxy.
|
||||
func (s *SOCKS4) Dial(network, addr string) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp4":
|
||||
default:
|
||||
return nil, errors.New("[socks4] no support for connection type " + network)
|
||||
}
|
||||
|
||||
c, err := s.dialer.Dial(network, s.addr)
|
||||
if err != nil {
|
||||
log.F("[socks4] dial to %s error: %s", s.addr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.connect(c, addr); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DialUDP connects to the given address via the proxy.
|
||||
func (s *SOCKS4) DialUDP(network, addr string) (pc net.PacketConn, err error) {
|
||||
return nil, proxy.ErrNotSupported
|
||||
}
|
||||
|
||||
func (s *SOCKS4) lookupIP(host string) (ip net.IP, err error) {
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
err = errors.New("[socks4] Cannot resolve host: " + host)
|
||||
return
|
||||
}
|
||||
ip = ips[0].To4()
|
||||
if len(ip) != net.IPv4len {
|
||||
err = errors.New("[socks4] IPv6 is not supported by socks4")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// connect takes an existing connection to a socks4 proxy server,
|
||||
// and commands the server to extend that connection to target,
|
||||
// which must be a canonical address with a host and port.
|
||||
func (s *SOCKS4) connect(conn net.Conn, target string) error {
|
||||
host, portStr, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return errors.New("[socks4] failed to parse port number: " + portStr)
|
||||
}
|
||||
|
||||
const baseBufSize = 8 + 1 // 1 is the len(userid)
|
||||
bufSize := baseBufSize
|
||||
var ip net.IP
|
||||
if ip = net.ParseIP(host); ip == nil {
|
||||
if s.socks4a {
|
||||
// The client should set the first three bytes of DSTIP to NULL
|
||||
// and the last byte to a non-zero value.
|
||||
ip = []byte{0, 0, 0, 1}
|
||||
bufSize += len(host) + 1
|
||||
} else {
|
||||
ip, err = s.lookupIP(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
return errors.New("[socks4] IPv6 is not supported by socks4")
|
||||
}
|
||||
}
|
||||
// taken from https://github.com/h12w/socks/blob/master/socks.go and https://en.wikipedia.org/wiki/SOCKS
|
||||
buf := pool.GetBuffer(bufSize)
|
||||
defer pool.PutBuffer(buf)
|
||||
copy(buf, []byte{
|
||||
Version,
|
||||
ConnectCommand,
|
||||
byte(port >> 8), // higher byte of destination port
|
||||
byte(port), // lower byte of destination port (big endian)
|
||||
ip[0], ip[1], ip[2], ip[3],
|
||||
0, // user id
|
||||
})
|
||||
if s.socks4a {
|
||||
copy(buf[baseBufSize:], host)
|
||||
buf[len(buf)-1] = 0
|
||||
}
|
||||
|
||||
resp := pool.GetBuffer(8)
|
||||
defer pool.PutBuffer(resp)
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("[socks4] failed to write greeting to socks4 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||
return errors.New("[socks4] failed to read greeting from socks4 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
switch resp[1] {
|
||||
case 0x5a:
|
||||
// request granted
|
||||
case 0x5b:
|
||||
err = errors.New("[socks4] connection request rejected or failed")
|
||||
case 0x5c:
|
||||
err = errors.New("[socks4] connection request request failed because client is not running identd (or not reachable from the server)")
|
||||
case 0x5d:
|
||||
err = errors.New("[socks4] connection request request failed because client's identd could not confirm the user ID in the request")
|
||||
default:
|
||||
err = errors.New("[socks4] connection request failed, unknown error")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.AddUsage("socks4", `
|
||||
Socks4 scheme:
|
||||
socks4://host:port
|
||||
`)
|
||||
}
|
179
proxy/socks5/client.go
Normal file
179
proxy/socks5/client.go
Normal file
@ -0,0 +1,179 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
"github.com/nadoo/glider/pkg/socks"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
proxy.RegisterDialer("socks5", NewSocks5Dialer)
|
||||
}
|
||||
|
||||
// NewSocks5Dialer returns a socks5 proxy dialer.
|
||||
func NewSocks5Dialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
|
||||
return NewSocks5(s, d, nil)
|
||||
}
|
||||
|
||||
// Addr returns forwarder's address.
|
||||
func (s *Socks5) Addr() string {
|
||||
if s.addr == "" {
|
||||
return s.dialer.Addr()
|
||||
}
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the network net via the SOCKS5 proxy.
|
||||
func (s *Socks5) Dial(network, addr string) (net.Conn, error) {
|
||||
c, err := s.dial(network, s.addr)
|
||||
if err != nil {
|
||||
log.F("[socks5]: dial to %s error: %s", s.addr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := s.connect(c, addr, socks.CmdConnect); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *Socks5) dial(network, addr string) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp6", "tcp4":
|
||||
default:
|
||||
return nil, errors.New("[socks5]: no support for connection type " + network)
|
||||
}
|
||||
|
||||
c, err := s.dialer.Dial(network, s.addr)
|
||||
if err != nil {
|
||||
log.F("[socks5]: dial to %s error: %s", s.addr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DialUDP connects to the given address via the proxy.
|
||||
func (s *Socks5) DialUDP(network, addr string) (pc net.PacketConn, err error) {
|
||||
c, err := s.dial("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.F("[socks5] dialudp dial tcp to %s error: %s", s.addr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var uAddr socks.Addr
|
||||
if uAddr, err = s.connect(c, addr, socks.CmdUDPAssociate); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := pool.GetBuffer(socks.MaxAddrLen)
|
||||
defer pool.PutBuffer(buf)
|
||||
|
||||
uAddress := uAddr.String()
|
||||
h, p, _ := net.SplitHostPort(uAddress)
|
||||
// if returned bind ip is unspecified
|
||||
if ip, err := netip.ParseAddr(h); err == nil && ip.IsUnspecified() {
|
||||
// indicate using conventional addr
|
||||
h, _, _ = net.SplitHostPort(s.addr)
|
||||
uAddress = net.JoinHostPort(h, p)
|
||||
}
|
||||
|
||||
pc, err = s.dialer.DialUDP(network, uAddress)
|
||||
if err != nil {
|
||||
log.F("[socks5] dialudp to %s error: %s", uAddress, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writeTo, err := net.ResolveUDPAddr("udp", uAddress)
|
||||
if err != nil {
|
||||
log.F("[socks5] resolve addr error: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewPktConn(pc, writeTo, socks.ParseAddr(addr), c), err
|
||||
}
|
||||
|
||||
// connect takes an existing connection to a socks5 proxy server,
|
||||
// and commands the server to extend that connection to target,
|
||||
// which must be a canonical address with a host and port.
|
||||
func (s *Socks5) connect(conn net.Conn, target string, cmd byte) (addr socks.Addr, err error) {
|
||||
// the size here is just an estimate
|
||||
buf := pool.GetBuffer(socks.MaxAddrLen)
|
||||
defer pool.PutBuffer(buf)
|
||||
|
||||
buf = append(buf[:0], Version)
|
||||
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
|
||||
buf = append(buf, 2 /* num auth methods */, socks.AuthNone, socks.AuthPassword)
|
||||
} else {
|
||||
buf = append(buf, 1 /* num auth methods */, socks.AuthNone)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return addr, errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return addr, errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
if buf[0] != Version {
|
||||
return addr, errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
|
||||
}
|
||||
if buf[1] == 0xff {
|
||||
return addr, errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
|
||||
}
|
||||
|
||||
if buf[1] == socks.AuthPassword {
|
||||
buf = buf[:0]
|
||||
buf = append(buf, 1 /* password protocol version */)
|
||||
buf = append(buf, uint8(len(s.user)))
|
||||
buf = append(buf, s.user...)
|
||||
buf = append(buf, uint8(len(s.password)))
|
||||
buf = append(buf, s.password...)
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return addr, errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return addr, errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if buf[1] != 0 {
|
||||
return addr, errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
|
||||
}
|
||||
}
|
||||
|
||||
buf = buf[:0]
|
||||
buf = append(buf, Version, cmd, 0 /* reserved */)
|
||||
buf = append(buf, socks.ParseAddr(target)...)
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return addr, errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
// read VER REP RSV
|
||||
if _, err := io.ReadFull(conn, buf[:3]); err != nil {
|
||||
return addr, errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
failure := "unknown error"
|
||||
if int(buf[1]) < len(socks.Errors) {
|
||||
failure = socks.Errors[buf[1]].Error()
|
||||
}
|
||||
|
||||
if len(failure) > 0 {
|
||||
return addr, errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
|
||||
}
|
||||
|
||||
return socks.ReadAddr(conn)
|
||||
}
|
127
proxy/socks5/packet.go
Normal file
127
proxy/socks5/packet.go
Normal file
@ -0,0 +1,127 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
"github.com/nadoo/glider/pkg/socks"
|
||||
)
|
||||
|
||||
// PktConn .
|
||||
type PktConn struct {
|
||||
net.PacketConn
|
||||
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 {
|
||||
pc := &PktConn{
|
||||
PacketConn: c,
|
||||
writeTo: writeAddr,
|
||||
target: targetAddr,
|
||||
ctrlConn: ctrlConn,
|
||||
}
|
||||
|
||||
if ctrlConn != nil {
|
||||
go func() {
|
||||
buf := pool.GetBuffer(1)
|
||||
defer pool.PutBuffer(buf)
|
||||
for {
|
||||
_, err := ctrlConn.Read(buf)
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
continue
|
||||
}
|
||||
// log.F("[socks5] dialudp udp associate end")
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return pc
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (pc *PktConn) readFrom(b []byte) (int, net.Addr, net.Addr, error) {
|
||||
buf := pool.GetBuffer(len(b))
|
||||
defer pool.PutBuffer(buf)
|
||||
|
||||
n, raddr, err := pc.PacketConn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return n, raddr, nil, 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
|
||||
// +----+------+------+----------+----------+----------+
|
||||
// |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")
|
||||
}
|
||||
|
||||
target, err := net.ResolveUDPAddr("udp", tgtAddr.String())
|
||||
if err != nil {
|
||||
return n, raddr, nil, errors.New("wrong target addr")
|
||||
}
|
||||
|
||||
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[3+len(tgtAddr):n])
|
||||
return n, raddr, target, err
|
||||
}
|
||||
|
||||
// WriteTo overrides the original function from net.PacketConn.
|
||||
func (pc *PktConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
target := pc.target
|
||||
if addr != nil {
|
||||
target = socks.ParseAddr(addr.String())
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
return 0, errors.New("invalid addr")
|
||||
}
|
||||
|
||||
buf := pool.GetBytesBuffer()
|
||||
defer pool.PutBytesBuffer(buf)
|
||||
|
||||
buf.Write([]byte{0, 0, 0})
|
||||
tgtLen, _ := buf.Write(target)
|
||||
buf.Write(b)
|
||||
|
||||
n, err := pc.PacketConn.WriteTo(buf.Bytes(), pc.writeTo)
|
||||
if n > tgtLen+3 {
|
||||
return n - tgtLen - 3, err
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Close .
|
||||
func (pc *PktConn) Close() error {
|
||||
if pc.ctrlConn != nil {
|
||||
pc.ctrlConn.Close()
|
||||
}
|
||||
|
||||
return pc.PacketConn.Close()
|
||||
}
|
299
proxy/socks5/server.go
Normal file
299
proxy/socks5/server.go
Normal file
@ -0,0 +1,299 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
"github.com/nadoo/glider/pkg/socks"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
var nm sync.Map
|
||||
|
||||
func init() {
|
||||
proxy.RegisterServer("socks5", NewSocks5Server)
|
||||
}
|
||||
|
||||
// NewSocks5Server returns a socks5 proxy server.
|
||||
func NewSocks5Server(s string, p proxy.Proxy) (proxy.Server, error) {
|
||||
return NewSocks5(s, nil, p)
|
||||
}
|
||||
|
||||
// ListenAndServe serves socks5 requests.
|
||||
func (s *Socks5) ListenAndServe() {
|
||||
go s.ListenAndServeUDP()
|
||||
s.ListenAndServeTCP()
|
||||
}
|
||||
|
||||
// ListenAndServeTCP listen and serve on tcp port.
|
||||
func (s *Socks5) ListenAndServeTCP() {
|
||||
l, err := net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.Fatalf("[socks5] failed to listen on %s: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.F("[socks5] listening TCP on %s", s.addr)
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
log.F("[socks5] failed to accept: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go s.Serve(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve serves a connection.
|
||||
func (s *Socks5) Serve(c net.Conn) {
|
||||
defer c.Close()
|
||||
|
||||
if c, ok := c.(*net.TCPConn); ok {
|
||||
c.SetKeepAlive(true)
|
||||
}
|
||||
|
||||
tgt, err := s.handshake(c)
|
||||
if err != nil {
|
||||
// UDP: keep the connection until disconnect then free the UDP socket
|
||||
if err == socks.Errors[9] {
|
||||
buf := pool.GetBuffer(1)
|
||||
defer pool.PutBuffer(buf)
|
||||
// block here
|
||||
for {
|
||||
_, err := c.Read(buf)
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
continue
|
||||
}
|
||||
// log.F("[socks5] servetcp udp associate end")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.F("[socks5] failed in handshake with %s: %v", c.RemoteAddr(), err)
|
||||
return
|
||||
}
|
||||
|
||||
rc, dialer, err := s.proxy.Dial("tcp", tgt.String())
|
||||
if err != nil {
|
||||
log.F("[socks5] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
log.F("[socks5] %s <-> %s via %s", c.RemoteAddr(), tgt, dialer.Addr())
|
||||
|
||||
if err = proxy.Relay(c, rc); err != nil {
|
||||
log.F("[socks5] %s <-> %s via %s, relay error: %v", c.RemoteAddr(), tgt, dialer.Addr(), err)
|
||||
// record remote conn failure only
|
||||
if !strings.Contains(err.Error(), s.addr) {
|
||||
s.proxy.Record(dialer, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServeUDP serves udp requests.
|
||||
func (s *Socks5) ListenAndServeUDP() {
|
||||
lc, err := net.ListenPacket("udp", s.addr)
|
||||
if err != nil {
|
||||
log.Fatalf("[socks5] failed to listen on UDP %s: %v", s.addr, err)
|
||||
return
|
||||
}
|
||||
defer lc.Close()
|
||||
|
||||
log.F("[socks5] listening UDP on %s", s.addr)
|
||||
|
||||
s.ServePacket(lc)
|
||||
}
|
||||
|
||||
// ServePacket implements proxy.PacketServer.
|
||||
func (s *Socks5) ServePacket(pc net.PacketConn) {
|
||||
for {
|
||||
c := NewPktConn(pc, nil, nil, nil)
|
||||
buf := pool.GetBuffer(proxy.UDPBufSize)
|
||||
|
||||
n, srcAddr, dstAddr, err := c.readFrom(buf)
|
||||
if err != nil {
|
||||
log.F("[socks5u] remote read error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var session *Session
|
||||
sessionKey := srcAddr.String()
|
||||
|
||||
v, ok := nm.Load(sessionKey)
|
||||
if !ok || v == nil {
|
||||
session = newSession(sessionKey, srcAddr, dstAddr, c)
|
||||
nm.Store(sessionKey, session)
|
||||
go s.serveSession(session)
|
||||
} else {
|
||||
session = v.(*Session)
|
||||
}
|
||||
|
||||
session.msgCh <- message{dstAddr, buf[:n]}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Socks5) serveSession(session *Session) {
|
||||
dstPC, dialer, err := s.proxy.DialUDP("udp", session.srcPC.target.String())
|
||||
if err != nil {
|
||||
log.F("[socks5u] remote dial error: %v", err)
|
||||
nm.Delete(session.key)
|
||||
return
|
||||
}
|
||||
defer dstPC.Close()
|
||||
|
||||
go func() {
|
||||
proxy.CopyUDP(session.srcPC, nil, dstPC, 2*time.Minute, 5*time.Second)
|
||||
nm.Delete(session.key)
|
||||
close(session.finCh)
|
||||
}()
|
||||
|
||||
log.F("[socks5u] %s <-> %s via %s", session.src, session.srcPC.target, dialer.Addr())
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-session.msgCh:
|
||||
_, err = dstPC.WriteTo(msg.msg, msg.dst)
|
||||
if err != nil {
|
||||
log.F("[socks5u] writeTo %s error: %v", msg.dst, err)
|
||||
}
|
||||
pool.PutBuffer(msg.msg)
|
||||
msg.msg = nil
|
||||
case <-session.finCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type message struct {
|
||||
dst net.Addr
|
||||
msg []byte
|
||||
}
|
||||
|
||||
// Session is a udp session
|
||||
type Session struct {
|
||||
key string
|
||||
src net.Addr
|
||||
dst net.Addr
|
||||
srcPC *PktConn
|
||||
msgCh chan message
|
||||
finCh chan struct{}
|
||||
}
|
||||
|
||||
func newSession(key string, src, dst net.Addr, srcPC *PktConn) *Session {
|
||||
return &Session{key, src, dst, srcPC, make(chan message, 32), make(chan struct{})}
|
||||
}
|
||||
|
||||
// Handshake fast-tracks SOCKS initialization to get target address to connect.
|
||||
func (s *Socks5) handshake(c net.Conn) (socks.Addr, error) {
|
||||
// Read RFC 1928 for request and reply structure and sizes
|
||||
buf := pool.GetBuffer(socks.MaxAddrLen)
|
||||
defer pool.PutBuffer(buf)
|
||||
|
||||
// read VER, NMETHODS, METHODS
|
||||
if _, err := io.ReadFull(c, buf[:2]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nmethods := buf[1]
|
||||
if _, err := io.ReadFull(c, buf[:nmethods]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// write VER METHOD
|
||||
if s.user != "" && s.password != "" {
|
||||
_, err := c.Write([]byte{Version, socks.AuthPassword})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = io.ReadFull(c, buf[:2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get username
|
||||
userLen := int(buf[1])
|
||||
if userLen <= 0 {
|
||||
c.Write([]byte{1, 1})
|
||||
return nil, errors.New("auth failed: wrong username length")
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(c, buf[:userLen]); err != nil {
|
||||
return nil, errors.New("auth failed: cannot get username")
|
||||
}
|
||||
user := string(buf[:userLen])
|
||||
|
||||
// Get password
|
||||
_, err = c.Read(buf[:1])
|
||||
if err != nil {
|
||||
return nil, errors.New("auth failed: cannot get password len")
|
||||
}
|
||||
|
||||
passLen := int(buf[0])
|
||||
if passLen <= 0 {
|
||||
c.Write([]byte{1, 1})
|
||||
return nil, errors.New("auth failed: wrong password length")
|
||||
}
|
||||
|
||||
_, err = io.ReadFull(c, buf[:passLen])
|
||||
if err != nil {
|
||||
return nil, errors.New("auth failed: cannot get password")
|
||||
}
|
||||
pass := string(buf[:passLen])
|
||||
|
||||
// Verify
|
||||
if user != s.user || pass != s.password {
|
||||
_, err = c.Write([]byte{1, 1})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.New("auth failed, authinfo: " + user + ":" + pass)
|
||||
}
|
||||
|
||||
// Response auth state
|
||||
_, err = c.Write([]byte{1, 0})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else if _, err := c.Write([]byte{Version, socks.AuthNone}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read VER CMD RSV ATYP DST.ADDR DST.PORT
|
||||
if _, err := io.ReadFull(c, buf[:3]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := buf[1]
|
||||
addr, err := socks.ReadAddr(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch cmd {
|
||||
case socks.CmdConnect:
|
||||
_, err = c.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}) // SOCKS v5, reply succeeded
|
||||
case socks.CmdUDPAssociate:
|
||||
listenAddr := socks.ParseAddr(c.LocalAddr().String())
|
||||
if listenAddr == nil { // maybe it's unix socket
|
||||
listenAddr = socks.ParseAddr("127.0.0.1:0")
|
||||
}
|
||||
_, err = c.Write(append([]byte{5, 0, 0}, listenAddr...)) // SOCKS v5, reply succeeded
|
||||
if err != nil {
|
||||
return nil, socks.Errors[7]
|
||||
}
|
||||
err = socks.Errors[9]
|
||||
default:
|
||||
return nil, socks.Errors[7]
|
||||
}
|
||||
|
||||
return addr, err // skip VER, CMD, RSV fields
|
||||
}
|
60
proxy/socks5/socks5.go
Normal file
60
proxy/socks5/socks5.go
Normal file
@ -0,0 +1,60 @@
|
||||
// https://www.rfc-editor.org/rfc/rfc1928
|
||||
|
||||
// socks5 client:
|
||||
// https://github.com/golang/net/tree/master/proxy
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package socks5 implements a socks5 proxy.
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/nadoo/glider/pkg/log"
|
||||
"github.com/nadoo/glider/proxy"
|
||||
)
|
||||
|
||||
// Version is socks5 version number.
|
||||
const Version = 5
|
||||
|
||||
// Socks5 is a base socks5 struct.
|
||||
type Socks5 struct {
|
||||
dialer proxy.Dialer
|
||||
proxy proxy.Proxy
|
||||
addr string
|
||||
user string
|
||||
password string
|
||||
}
|
||||
|
||||
// NewSocks5 returns a Proxy that makes SOCKS v5 connections to the given address.
|
||||
// with an optional username and password. (RFC 1928)
|
||||
func NewSocks5(s string, d proxy.Dialer, p proxy.Proxy) (*Socks5, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
log.F("parse err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := u.Host
|
||||
user := u.User.Username()
|
||||
pass, _ := u.User.Password()
|
||||
|
||||
h := &Socks5{
|
||||
dialer: d,
|
||||
proxy: p,
|
||||
addr: addr,
|
||||
user: user,
|
||||
password: pass,
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy.AddUsage("socks5", `
|
||||
Socks5 scheme:
|
||||
socks5://[user:pass@]host:port
|
||||
`)
|
||||
}
|
143
proxy/ss/cipher/cipher.go
Normal file
143
proxy/ss/cipher/cipher.go
Normal file
@ -0,0 +1,143 @@
|
||||
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]
|
||||
}
|
94
proxy/ss/cipher/shadowaead/cipher.go
Normal file
94
proxy/ss/cipher/shadowaead/cipher.go
Normal file
@ -0,0 +1,94 @@
|
||||
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
|
||||
}
|
67
proxy/ss/cipher/shadowaead/conn.go
Normal file
67
proxy/ss/cipher/shadowaead/conn.go
Normal file
@ -0,0 +1,67 @@
|
||||
package shadowaead
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
type streamConn struct {
|
||||
net.Conn
|
||||
Cipher
|
||||
r *reader
|
||||
w *writer
|
||||
}
|
||||
|
||||
// NewConn wraps a stream-oriented net.Conn with cipher.
|
||||
func NewConn(c net.Conn, ciph Cipher) net.Conn { return &streamConn{Conn: c, Cipher: ciph} }
|
||||
|
||||
func (c *streamConn) initReader() error {
|
||||
salt := make([]byte, c.SaltSize())
|
||||
if _, err := io.ReadFull(c.Conn, salt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aead, err := c.Decrypter(salt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.r = newReader(c.Conn, aead)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *streamConn) Read(b []byte) (int, error) {
|
||||
if c.r == nil {
|
||||
if err := c.initReader(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return c.r.Read(b)
|
||||
}
|
||||
|
||||
func (c *streamConn) initWriter() error {
|
||||
salt := make([]byte, c.SaltSize())
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
return err
|
||||
}
|
||||
aead, err := c.Encrypter(salt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.Conn.Write(salt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.w = newWriter(c.Conn, aead)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *streamConn) Write(b []byte) (int, error) {
|
||||
if c.w == nil {
|
||||
if err := c.initWriter(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return c.w.Write(b)
|
||||
}
|
97
proxy/ss/cipher/shadowaead/packet.go
Normal file
97
proxy/ss/cipher/shadowaead/packet.go
Normal file
@ -0,0 +1,97 @@
|
||||
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
|
||||
}
|
150
proxy/ss/cipher/shadowaead/stream.go
Normal file
150
proxy/ss/cipher/shadowaead/stream.go
Normal file
@ -0,0 +1,150 @@
|
||||
// protocol:
|
||||
// format: [encrypted payload length] [Overhead] [encrypted payload] [Overhead]
|
||||
// sizes: 2 bytes, aead.Overhead() bytes, n bytes, aead.Overhead() bytes
|
||||
// max(n): 0x3FFF
|
||||
|
||||
package shadowaead
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
lenSize = 2
|
||||
// max payload size: 16383 from ss-libev aead.c: CHUNK_SIZE_MASK
|
||||
maxPayload = 0x3FFF
|
||||
// buf size, shoud enough to save lenSize + aead.Overhead() + maxPayload + aead.Overhead()
|
||||
bufSize = 17 << 10
|
||||
)
|
||||
|
||||
type writer struct {
|
||||
io.Writer
|
||||
cipher.AEAD
|
||||
nonce [32]byte
|
||||
}
|
||||
|
||||
// NewWriter wraps an io.Writer with AEAD encryption.
|
||||
func NewWriter(w io.Writer, aead cipher.AEAD) io.Writer { return newWriter(w, aead) }
|
||||
|
||||
func newWriter(w io.Writer, aead cipher.AEAD) *writer {
|
||||
return &writer{Writer: w, AEAD: aead}
|
||||
}
|
||||
|
||||
// Write encrypts p and writes to the embedded io.Writer.
|
||||
func (w *writer) Write(p []byte) (n int, err error) {
|
||||
buf := pool.GetBuffer(bufSize)
|
||||
defer pool.PutBuffer(buf)
|
||||
|
||||
nonce := w.nonce[:w.NonceSize()]
|
||||
encLenSize := lenSize + w.Overhead()
|
||||
for nw := maxPayload; n < len(p); n += nw {
|
||||
if left := len(p) - n; left < maxPayload {
|
||||
nw = left
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(buf[:lenSize], uint16(nw))
|
||||
w.Seal(buf[:0], nonce, buf[:lenSize], nil)
|
||||
increment(nonce)
|
||||
|
||||
w.Seal(buf[:encLenSize], nonce, p[n:n+nw], nil)
|
||||
increment(nonce)
|
||||
|
||||
if _, err = w.Writer.Write(buf[:encLenSize+nw+w.Overhead()]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
io.Reader
|
||||
cipher.AEAD
|
||||
nonce [32]byte
|
||||
buf []byte
|
||||
offset int
|
||||
}
|
||||
|
||||
// NewReader wraps an io.Reader with AEAD decryption.
|
||||
func NewReader(r io.Reader, aead cipher.AEAD) io.Reader { return newReader(r, aead) }
|
||||
|
||||
func newReader(r io.Reader, aead cipher.AEAD) *reader {
|
||||
return &reader{Reader: r, AEAD: aead}
|
||||
}
|
||||
|
||||
// NOTE: len(p) MUST >= max payload size + AEAD overhead.
|
||||
func (r *reader) read(p []byte) (int, error) {
|
||||
nonce := r.nonce[:r.NonceSize()]
|
||||
|
||||
// read encrypted lenSize + overhead
|
||||
p = p[:lenSize+r.Overhead()]
|
||||
if _, err := io.ReadFull(r.Reader, p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// decrypt lenSize
|
||||
_, err := r.Open(p[:0], nonce, p, nil)
|
||||
increment(nonce)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// get payload size
|
||||
size := int(binary.BigEndian.Uint16(p[:lenSize]))
|
||||
|
||||
// read encrypted payload + overhead
|
||||
p = p[:size+r.Overhead()]
|
||||
if _, err := io.ReadFull(r.Reader, p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// decrypt payload
|
||||
_, err = r.Open(p[:0], nonce, p, nil)
|
||||
increment(nonce)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (r *reader) Read(p []byte) (int, error) {
|
||||
if r.buf == nil {
|
||||
if len(p) >= maxPayload+r.Overhead() {
|
||||
return r.read(p)
|
||||
}
|
||||
|
||||
buf := pool.GetBuffer(bufSize)
|
||||
n, err := r.read(buf)
|
||||
if err != nil {
|
||||
pool.PutBuffer(buf)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
r.buf = buf[:n]
|
||||
r.offset = 0
|
||||
}
|
||||
|
||||
n := copy(p, r.buf[r.offset:])
|
||||
r.offset += n
|
||||
if r.offset == len(r.buf) {
|
||||
pool.PutBuffer(r.buf)
|
||||
r.buf = nil
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// increment little-endian encoded unsigned integer b. Wrap around on overflow.
|
||||
func increment(b []byte) {
|
||||
for i := range b {
|
||||
b[i]++
|
||||
if b[i] != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
143
proxy/ss/cipher/shadowstream/cipher.go
Normal file
143
proxy/ss/cipher/shadowstream/cipher.go
Normal file
@ -0,0 +1,143 @@
|
||||
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
|
||||
}
|
62
proxy/ss/cipher/shadowstream/conn.go
Normal file
62
proxy/ss/cipher/shadowstream/conn.go
Normal file
@ -0,0 +1,62 @@
|
||||
package shadowstream
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
type conn struct {
|
||||
net.Conn
|
||||
Cipher
|
||||
r *reader
|
||||
w *writer
|
||||
}
|
||||
|
||||
// NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption.
|
||||
func NewConn(c net.Conn, ciph Cipher) net.Conn {
|
||||
return &conn{Conn: c, Cipher: ciph}
|
||||
}
|
||||
|
||||
func (c *conn) initReader() error {
|
||||
if c.r == nil {
|
||||
iv := make([]byte, c.IVSize())
|
||||
if _, err := io.ReadFull(c.Conn, iv); err != nil {
|
||||
return err
|
||||
}
|
||||
c.r = &reader{Reader: c.Conn, Stream: c.Decrypter(iv)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) Read(b []byte) (int, error) {
|
||||
if c.r == nil {
|
||||
if err := c.initReader(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return c.r.Read(b)
|
||||
}
|
||||
|
||||
func (c *conn) initWriter() error {
|
||||
if c.w == nil {
|
||||
iv := make([]byte, c.IVSize())
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.Conn.Write(iv); err != nil {
|
||||
return err
|
||||
}
|
||||
c.w = &writer{Writer: c.Conn, Stream: c.Encrypter(iv)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) Write(b []byte) (int, error) {
|
||||
if c.w == nil {
|
||||
if err := c.initWriter(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return c.w.Write(b)
|
||||
}
|
80
proxy/ss/cipher/shadowstream/packet.go
Normal file
80
proxy/ss/cipher/shadowstream/packet.go
Normal file
@ -0,0 +1,80 @@
|
||||
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
|
||||
}
|
55
proxy/ss/cipher/shadowstream/stream.go
Normal file
55
proxy/ss/cipher/shadowstream/stream.go
Normal file
@ -0,0 +1,55 @@
|
||||
package shadowstream
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"io"
|
||||
|
||||
"github.com/nadoo/glider/pkg/pool"
|
||||
)
|
||||
|
||||
const bufSize = 32 * 1024
|
||||
|
||||
type writer struct {
|
||||
io.Writer
|
||||
cipher.Stream
|
||||
}
|
||||
|
||||
// NewWriter wraps an io.Writer with stream cipher encryption.
|
||||
func NewWriter(w io.Writer, s cipher.Stream) io.Writer {
|
||||
return &writer{Writer: w, Stream: s}
|
||||
}
|
||||
|
||||
func (w *writer) Write(p []byte) (n int, err error) {
|
||||
buf := pool.GetBuffer(bufSize)
|
||||
defer pool.PutBuffer(buf)
|
||||
|
||||
for nw := 0; n < len(p) && err == nil; n += nw {
|
||||
end := n + len(buf)
|
||||
if end > len(p) {
|
||||
end = len(p)
|
||||
}
|
||||
w.XORKeyStream(buf, p[n:end])
|
||||
nw, err = w.Writer.Write(buf[:end-n])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
io.Reader
|
||||
cipher.Stream
|
||||
}
|
||||
|
||||
// NewReader wraps an io.Reader with stream cipher decryption.
|
||||
func NewReader(r io.Reader, s cipher.Stream) io.Reader {
|
||||
return &reader{Reader: r, Stream: s}
|
||||
}
|
||||
|
||||
func (r *reader) Read(p []byte) (int, error) {
|
||||
n, err := r.Reader.Read(p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
p = p[:n]
|
||||
r.XORKeyStream(p, p)
|
||||
return n, nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user