Compare commits

...

12 Commits

Author SHA1 Message Date
88c7ccf749 Add: license scan report and status
* Add license scan report and status

Signed-off-by: fossabot <badges@fossa.io>

* Improve: adjust indentation and position
2018-07-15 22:32:20 +08:00
3cacfb8a7f Update: add config route 2018-07-15 22:23:20 +08:00
0eef9bbf5d Add: selector and proxys & rules router 2018-07-12 23:28:38 +08:00
39b45513af Improve: delete useless code and code coverage is now 100% 2018-07-12 18:03:02 +08:00
283e4e1f4f Update: README.md logo and badges 2018-06-23 00:44:28 +08:00
bd0a1ae1a7 Improve: gofmt -s for shadowsocks.go 2018-06-22 23:56:41 +08:00
018a6ba041 Improve: add log level 2018-06-22 13:15:20 +08:00
9c2ace1f91 Fixed: handle chunked data 2018-06-22 10:26:56 +08:00
17224a32cb Add: add windows release 2018-06-20 23:00:16 +08:00
05ab653103 Add: config hub route 2018-06-20 22:41:02 +08:00
c2c8f82f96 Fixed: the configuration can now be updated correctly 2018-06-19 20:31:36 +08:00
330a3391e3 Improve: ci config and Dockerfile & update dependencies 2018-06-18 18:24:11 +08:00
38 changed files with 925 additions and 144 deletions

View File

@ -11,7 +11,7 @@ env:
- NAME=clash
- BINDIR=bin
script:
- go test
- go test ./...
before_deploy: make -j releases
deploy:
provider: releases

View File

@ -13,7 +13,7 @@ RUN dep ensure && \
FROM alpine:latest
RUN apk --no-cache add ca-certificates && \
mkdir -p /root/.config/clash
COPY --from=builder /clash .
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash .
EXPOSE 7890 7891
ENTRYPOINT ["/clash"]

16
Gopkg.lock generated
View File

@ -13,6 +13,18 @@
revision = "44cc805cf13205b55f69e14bcb69867d1ae92f98"
version = "v1.1.0"
[[projects]]
name = "github.com/go-chi/chi"
packages = ["."]
revision = "e83ac2304db3c50cf03d96a2fcd39009d458bc35"
version = "v3.3.2"
[[projects]]
name = "github.com/go-chi/render"
packages = ["."]
revision = "3215478343fbc559bd3fc08f7031bb134d6bdad5"
version = "v1.0.1"
[[projects]]
name = "github.com/oschwald/geoip2-golang"
packages = ["."]
@ -53,7 +65,7 @@
"poly1305",
"ssh/terminal"
]
revision = "a8fb68e7206f8c78be19b432c58eb52a6aa34462"
revision = "027cca12c2d63e3d62b670d901e8a2c95854feec"
[[projects]]
branch = "master"
@ -80,6 +92,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "4297c505508c6cdd8c94fd4ef29bc6940c65fea81e125fcf871a316b6e671a71"
inputs-digest = "c3c901e4e393a2df9e421924d3a4d85ec73642e36dcbc1ddca5fc13159220e86"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -25,6 +25,14 @@
# unused-packages = true
[[constraint]]
name = "github.com/go-chi/chi"
version = "3.3.2"
[[constraint]]
name = "github.com/go-chi/render"
version = "1.0.1"
[[constraint]]
name = "github.com/oschwald/geoip2-golang"
version = "1.2.1"

View File

@ -10,10 +10,14 @@ linux:
macos:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
releases: linux macos
win64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
releases: linux macos win64
chmod +x $(BINDIR)/$(NAME)-*
gzip $(BINDIR)/$(NAME)-linux
gzip $(BINDIR)/$(NAME)-macos
zip -m -j $(BINDIR)/$(NAME)-win64.zip $(BINDIR)/$(NAME)-win64.exe
clean:
rm $(BINDIR)/*

View File

@ -1,8 +1,25 @@
# Clash
<h1 align="center">
<img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200">
<br>
Clash
<br>
</h1>
[![TravisCI](https://img.shields.io/travis/Dreamacro/clash.svg?style=flat-square)](https://travis-ci.org/Dreamacro/clash)
<h4 align="center">A rule based proxy in Go.</h4>
A rule based proxy in Go.
<p align="center">
<a href="https://travis-ci.org/Dreamacro/clash">
<img src="https://img.shields.io/travis/Dreamacro/clash.svg?style=flat-square"
alt="Travis-CI">
</a>
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</a>
<a href="https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_shield" alt="FOSSA Status"><img src="https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=shield"/></a>
<a href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
</a>
</p>
## Features
@ -72,6 +89,9 @@ GEOIP,CN,DIRECT
FINAL,,Proxy # note: there is two ","
```
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
## TODO
- [ ] Complementing the necessary rule operators

View File

@ -35,6 +35,10 @@ func (d *Direct) Name() string {
return "Direct"
}
func (d *Direct) Type() C.AdapterType {
return C.Direct
}
func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", net.JoinHostPort(addr.String(), addr.Port))
if err != nil {

View File

@ -31,6 +31,10 @@ func (r *Reject) Name() string {
return "Reject"
}
func (r *Reject) Type() C.AdapterType {
return C.Reject
}
func (r *Reject) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return &RejectAdapter{}, nil
}

65
adapters/selector.go Normal file
View File

@ -0,0 +1,65 @@
package adapters
import (
"errors"
C "github.com/Dreamacro/clash/constant"
)
type Selector struct {
name string
selected C.Proxy
proxys map[string]C.Proxy
}
func (s *Selector) Name() string {
return s.name
}
func (s *Selector) Type() C.AdapterType {
return C.Selector
}
func (s *Selector) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return s.selected.Generator(addr)
}
func (s *Selector) Now() string {
return s.selected.Name()
}
func (s *Selector) All() []string {
var all []string
for k, _ := range s.proxys {
all = append(all, k)
}
return all
}
func (s *Selector) Set(name string) error {
proxy, exist := s.proxys[name]
if !exist {
return errors.New("Proxy does not exist")
}
s.selected = proxy
return nil
}
func NewSelector(name string, proxys map[string]C.Proxy) (*Selector, error) {
if len(proxys) == 0 {
return nil, errors.New("Provide at least one proxy")
}
mapping := make(map[string]C.Proxy)
var init string
for k, v := range proxys {
mapping[k] = v
init = k
}
s := &Selector{
name: name,
proxys: mapping,
selected: proxys[init],
}
return s, nil
}

View File

@ -44,6 +44,10 @@ func (ss *ShadowSocks) Name() string {
return ss.name
}
func (ss *ShadowSocks) Type() C.AdapterType {
return C.Shadowsocks
}
func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.server)
if err != nil {
@ -93,13 +97,13 @@ func serializesSocksAddr(addr *C.Addr) []byte {
case socks.AtypDomainName:
len := uint8(len(addr.Host))
host := []byte(addr.Host)
buf = [][]byte{[]byte{aType, len}, host, port}
buf = [][]byte{{aType, len}, host, port}
case socks.AtypIPv4:
host := addr.IP.To4()
buf = [][]byte{[]byte{aType}, host, port}
buf = [][]byte{{aType}, host, port}
case socks.AtypIPv6:
host := addr.IP.To16()
buf = [][]byte{[]byte{aType}, host, port}
buf = [][]byte{{aType}, host, port}
}
return bytes.Join(buf, []byte(""))
}

View File

@ -19,21 +19,40 @@ type URLTest struct {
addr *C.Addr
fast C.Proxy
delay time.Duration
done chan struct{}
}
func (u *URLTest) Name() string {
return u.name
}
func (u *URLTest) Type() C.AdapterType {
return C.URLTest
}
func (u *URLTest) Now() string {
return u.fast.Name()
}
func (u *URLTest) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return u.fast.Generator(addr)
}
func (u *URLTest) Close() {
u.done <- struct{}{}
}
func (u *URLTest) loop() {
tick := time.Tick(u.delay)
tick := time.NewTicker(u.delay)
go u.speedTest()
for range tick {
go u.speedTest()
Loop:
for {
select {
case <-tick.C:
go u.speedTest()
case <-u.done:
break Loop
}
}
}
@ -142,6 +161,7 @@ func NewURLTest(name string, proxys []C.Proxy, rawURL string, delay time.Duratio
addr: addr,
fast: proxys[0],
delay: delay,
done: make(chan struct{}),
}
go urlTest.loop()
return urlTest, nil

View File

@ -5,6 +5,15 @@ import (
"net"
)
// Adapter Type
const (
Direct AdapterType = iota
Reject
Selector
Shadowsocks
URLTest
)
type ProxyAdapter interface {
ReadWriter() io.ReadWriter
Conn() net.Conn
@ -19,5 +28,26 @@ type ServerAdapter interface {
type Proxy interface {
Name() string
Type() AdapterType
Generator(addr *Addr) (ProxyAdapter, error)
}
// AdapterType is enum of adapter type
type AdapterType int
func (at AdapterType) String() string {
switch at {
case Direct:
return "Direct"
case Reject:
return "Reject"
case Selector:
return "Selector"
case Shadowsocks:
return "Shadowsocks"
case URLTest:
return "URLTest"
default:
return "Unknow"
}
}

View File

@ -16,8 +16,8 @@ import (
const (
Name = "clash"
DefalutHTTPPort = "7890"
DefalutSOCKSPort = "7891"
DefalutHTTPPort = 7890
DefalutSOCKSPort = 7891
)
var (
@ -26,6 +26,13 @@ var (
MMDBPath string
)
type General struct {
Mode *string `json:"mode,omitempty"`
AllowLan *bool `json:"allow-lan,omitempty"`
Port *int `json:"port,omitempty"`
SocksPort *int `json:"socks-port,omitempty"`
}
func init() {
currentUser, err := user.Current()
if err != nil {

7
constant/proxy.go Normal file
View File

@ -0,0 +1,7 @@
package constant
// ProxySignal is used to handle graceful shutdown of proxy
type ProxySignal struct {
Done chan<- struct{}
Closed <-chan struct{}
}

View File

@ -11,8 +11,26 @@ const (
type RuleType int
func (rt RuleType) String() string {
switch rt {
case DomainSuffix:
return "DomainSuffix"
case DomainKeyword:
return "DomainKeyword"
case GEOIP:
return "GEOIP"
case IPCIDR:
return "IPCIDR"
case FINAL:
return "FINAL"
default:
return "Unknow"
}
}
type Rule interface {
RuleType() RuleType
IsMatch(addr *Addr) bool
Adapter() string
Payload() string
}

BIN
docs/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

31
hub/common.go Normal file
View File

@ -0,0 +1,31 @@
package hub
import (
"github.com/Dreamacro/clash/proxy"
T "github.com/Dreamacro/clash/tunnel"
)
var (
tunnel = T.GetInstance()
listener = proxy.Instance()
)
type Error struct {
Error string `json:"error"`
}
type Errors struct {
Errors map[string]string `json:"errors"`
}
func formatErrors(errorsMap map[string]error) (bool, Errors) {
errors := make(map[string]string)
hasError := false
for key, err := range errorsMap {
if err != nil {
errors[key] = err.Error()
hasError = true
}
}
return hasError, Errors{Errors: errors}
}

74
hub/configs.go Normal file
View File

@ -0,0 +1,74 @@
package hub
import (
"fmt"
"net/http"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/proxy"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func configRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getConfigs)
r.Put("/", updateConfigs)
return r
}
var modeMapping = map[string]T.Mode{
"Global": T.Global,
"Rule": T.Rule,
"Direct": T.Direct,
}
func getConfigs(w http.ResponseWriter, r *http.Request) {
info := listener.Info()
mode := tunnel.GetMode().String()
info.Mode = &mode
render.JSON(w, r, info)
}
func updateConfigs(w http.ResponseWriter, r *http.Request) {
general := &C.General{}
err := render.DecodeJSON(r.Body, general)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
// update errors
var proxyErr, modeErr error
// update proxy
listener := proxy.Instance()
proxyErr = listener.Update(general.AllowLan, general.Port, general.SocksPort)
// update mode
if general.Mode != nil {
mode, ok := modeMapping[*general.Mode]
if !ok {
modeErr = fmt.Errorf("Mode error")
} else {
tunnel.SetMode(mode)
}
}
hasError, errors := formatErrors(map[string]error{
"proxy": proxyErr,
"mode": modeErr,
})
if hasError {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, errors)
return
}
w.WriteHeader(http.StatusNoContent)
}

129
hub/proxys.go Normal file
View File

@ -0,0 +1,129 @@
package hub
import (
"fmt"
"net/http"
A "github.com/Dreamacro/clash/adapters"
C "github.com/Dreamacro/clash/constant"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func proxyRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProxys)
r.Get("/{name}", getProxy)
r.Put("/{name}", updateProxy)
return r
}
type SampleProxy struct {
Type string `json:"type"`
}
type Selector struct {
Type string `json:"type"`
Now string `json:"now"`
All []string `json:"all"`
}
type URLTest struct {
Type string `json:"type"`
Now string `json:"now"`
}
func transformProxy(proxy C.Proxy) interface{} {
t := proxy.Type()
switch t {
case C.Selector:
selector := proxy.(*A.Selector)
return Selector{
Type: t.String(),
Now: selector.Now(),
All: selector.All(),
}
case C.URLTest:
return URLTest{
Type: t.String(),
Now: proxy.(*A.URLTest).Now(),
}
default:
return SampleProxy{
Type: proxy.Type().String(),
}
}
}
type GetProxysResponse struct {
Proxys map[string]interface{} `json:"proxys"`
}
func getProxys(w http.ResponseWriter, r *http.Request) {
_, rawProxys := tunnel.Config()
proxys := make(map[string]interface{})
for name, proxy := range rawProxys {
proxys[name] = transformProxy(proxy)
}
render.JSON(w, r, GetProxysResponse{Proxys: proxys})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
_, proxys := tunnel.Config()
proxy, exist := proxys[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
render.JSON(w, r, transformProxy(proxy))
}
type UpdateProxyRequest struct {
Name string `json:"name"`
}
func updateProxy(w http.ResponseWriter, r *http.Request) {
req := UpdateProxyRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
name := chi.URLParam(r, "name")
_, proxys := tunnel.Config()
proxy, exist := proxys[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
selector, ok := proxy.(*A.Selector)
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Proxy can't update",
})
return
}
if err := selector.Set(req.Name); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: fmt.Sprintf("Selector update error: %s", err.Error()),
})
return
}
w.WriteHeader(http.StatusNoContent)
}

53
hub/rules.go Normal file
View File

@ -0,0 +1,53 @@
package hub
import (
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func ruleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getRules)
r.Put("/", updateRules)
return r
}
type Rule struct {
Name string `json:"name"`
Payload string `json:"type"`
}
type GetRulesResponse struct {
Rules []Rule `json:"rules"`
}
func getRules(w http.ResponseWriter, r *http.Request) {
rulesCfg, _ := tunnel.Config()
var rules []Rule
for _, rule := range rulesCfg {
rules = append(rules, Rule{
Name: rule.RuleType().String(),
Payload: rule.Payload(),
})
}
w.WriteHeader(http.StatusOK)
render.JSON(w, r, GetRulesResponse{
Rules: rules,
})
}
func updateRules(w http.ResponseWriter, r *http.Request) {
err := tunnel.UpdateConfig()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
w.WriteHeader(http.StatusNoContent)
}

View File

@ -5,36 +5,26 @@ import (
"net/http"
"time"
"github.com/Dreamacro/clash/tunnel"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/render"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.GetInstance()
)
type Traffic struct {
Up int64 `json:"up"`
Down int64 `json:"down"`
}
type Log struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
type Error struct {
Error string `json:"error"`
}
func NewHub(addr string) {
r := chi.NewRouter()
r.Get("/traffic", traffic)
r.Get("/logs", getLogs)
r.Mount("/configs", configRouter())
r.Mount("/proxys", proxyRouter())
r.Mount("/rules", ruleRouter())
err := http.ListenAndServe(addr, r)
if err != nil {
@ -43,10 +33,10 @@ func NewHub(addr string) {
}
func traffic(w http.ResponseWriter, r *http.Request) {
render.Status(r, http.StatusOK)
w.WriteHeader(http.StatusOK)
tick := time.NewTicker(time.Second)
t := tun.Traffic()
t := tunnel.Traffic()
for range tick.C {
up, down := t.Now()
if err := json.NewEncoder(w).Encode(Traffic{
@ -59,19 +49,55 @@ func traffic(w http.ResponseWriter, r *http.Request) {
}
}
type GetLogs struct {
Level string `json:"level"`
}
type Log struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
func getLogs(w http.ResponseWriter, r *http.Request) {
src := tun.Log()
req := &GetLogs{}
render.DecodeJSON(r.Body, req)
if req.Level == "" {
req.Level = "info"
}
mapping := map[string]T.LogLevel{
"info": T.INFO,
"debug": T.DEBUG,
"error": T.ERROR,
"warning": T.WARNING,
}
level, ok := mapping[req.Level]
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Level error",
})
return
}
src := tunnel.Log()
sub, err := src.Subscribe()
defer src.UnSubscribe(sub)
if err != nil {
render.Status(r, http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
render.Status(r, http.StatusOK)
for elm := range sub {
log := elm.(tunnel.Log)
log := elm.(T.Log)
if log.LogLevel > level {
continue
}
if err := json.NewEncoder(w).Encode(Log{
Type: log.Type(),
Payload: log.Payload,

30
main.go
View File

@ -7,38 +7,28 @@ import (
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/hub"
"github.com/Dreamacro/clash/proxy/http"
"github.com/Dreamacro/clash/proxy/socks"
"github.com/Dreamacro/clash/proxy"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
func main() {
if err := tunnel.GetInstance().UpdateConfig(); err != nil {
log.Fatalf("Parse config error: %s", err.Error())
}
if err := proxy.Instance().Run(); err != nil {
log.Fatalf("Proxy listen error: %s", err.Error())
}
// Hub
cfg, err := C.GetConfig()
if err != nil {
log.Fatalf("Read config error: %s", err.Error())
}
port, socksPort := C.DefalutHTTPPort, C.DefalutSOCKSPort
section := cfg.Section("General")
if key, err := section.GetKey("port"); err == nil {
port = key.Value()
}
if key, err := section.GetKey("socks-port"); err == nil {
socksPort = key.Value()
}
err = tunnel.GetInstance().UpdateConfig()
if err != nil {
log.Fatalf("Parse config error: %s", err.Error())
}
go http.NewHttpProxy(port)
go socks.NewSocksProxy(socksPort)
// Hub
if key, err := section.GetKey("external-controller"); err == nil {
go hub.NewHub(key.Value())
}

View File

@ -1,18 +1,3 @@
package observable
import (
"errors"
)
type Iterable <-chan interface{}
func NewIterable(any interface{}) (Iterable, error) {
switch any := any.(type) {
case chan interface{}:
return Iterable(any), nil
case <-chan interface{}:
return Iterable(any), nil
default:
return nil, errors.New("type error")
}
}

View File

@ -50,7 +50,6 @@ func (o *Observable) Subscribe() (Subscription, error) {
func (o *Observable) UnSubscribe(sub Subscription) {
elm, exist := o.listener.Load(sub)
if !exist {
println("not exist")
return
}
subscriber := elm.(*Subscriber)

View File

@ -27,11 +27,7 @@ func TestObservable(t *testing.T) {
t.Error(err)
}
count := 0
for {
_, open := <-data
if !open {
break
}
for range data {
count = count + 1
}
if count != 5 {
@ -49,11 +45,7 @@ func TestObservable_MutilSubscribe(t *testing.T) {
var wg sync.WaitGroup
wg.Add(2)
waitCh := func(ch <-chan interface{}) {
for {
_, open := <-ch
if !open {
break
}
for range ch {
count = count + 1
}
wg.Done()
@ -80,6 +72,25 @@ func TestObservable_UnSubscribe(t *testing.T) {
}
}
func TestObservable_SubscribeClosedSource(t *testing.T) {
iter := iterator([]interface{}{1})
src := NewObservable(iter)
data, _ := src.Subscribe()
<-data
_, closed := src.Subscribe()
if closed == nil {
t.Error("Observable should be closed")
}
}
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
sub := Subscription(make(chan interface{}))
iter := iterator([]interface{}{1})
src := NewObservable(iter)
src.UnSubscribe(sub)
}
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
// waiting for other goroutine recycle
time.Sleep(120 * time.Millisecond)
@ -97,11 +108,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
var wg sync.WaitGroup
wg.Add(max)
waitCh := func(ch <-chan interface{}) {
for {
_, open := <-ch
if !open {
break
}
for range ch {
}
wg.Done()
}

View File

@ -1,15 +0,0 @@
package observable
func mergeWithBytes(ch <-chan interface{}, buf []byte) chan interface{} {
out := make(chan interface{})
go func() {
defer close(out)
if len(buf) != 0 {
out <- buf
}
for elm := range ch {
out <- elm
}
}()
return out
}

View File

@ -32,7 +32,6 @@ func (h *HttpAdapter) Connect(proxy C.ProxyAdapter) {
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
resp, err := req.RoundTrip(h.r)
@ -48,7 +47,23 @@ func (h *HttpAdapter) Connect(proxy C.ProxyAdapter) {
}
}
h.w.WriteHeader(resp.StatusCode)
io.Copy(h.w, resp.Body)
var writer io.Writer = h.w
if len(resp.TransferEncoding) > 0 && resp.TransferEncoding[0] == "chunked" {
writer = ChunkWriter{Writer: h.w}
}
io.Copy(writer, resp.Body)
}
type ChunkWriter struct {
io.Writer
}
func (cw ChunkWriter) Write(b []byte) (int, error) {
n, err := cw.Writer.Write(b)
if err == nil {
cw.Writer.(http.Flusher).Flush()
}
return n, err
}
func NewHttp(host string, w http.ResponseWriter, r *http.Request) (*HttpAdapter, chan struct{}) {

View File

@ -1,7 +1,7 @@
package http
import (
"fmt"
"context"
"net"
"net/http"
"strings"
@ -17,9 +17,20 @@ var (
tun = tunnel.GetInstance()
)
func NewHttpProxy(port string) {
func NewHttpProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
server := &http.Server{
Addr: fmt.Sprintf(":%s", port),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
handleTunneling(w, r)
@ -28,8 +39,20 @@ func NewHttpProxy(port string) {
}
}),
}
log.Infof("HTTP proxy :%s", port)
server.ListenAndServe()
go func() {
log.Infof("HTTP proxy listening at: %s", addr)
server.Serve(l)
}()
go func() {
<-done
server.Shutdown(context.Background())
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleHTTP(w http.ResponseWriter, r *http.Request) {

143
proxy/listener.go Normal file
View File

@ -0,0 +1,143 @@
package proxy
import (
"fmt"
"sync"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/proxy/http"
"github.com/Dreamacro/clash/proxy/socks"
log "github.com/sirupsen/logrus"
)
var (
listener *Listener
once sync.Once
)
type Listener struct {
httpPort int
socksPort int
allowLan bool
// signal for update
httpSignal *C.ProxySignal
socksSignal *C.ProxySignal
}
// Info returns the proxys's current configuration
func (l *Listener) Info() (info C.General) {
return C.General{
Port: &l.httpPort,
SocksPort: &l.socksPort,
AllowLan: &l.allowLan,
}
}
func (l *Listener) Update(allowLan *bool, httpPort *int, socksPort *int) error {
if allowLan != nil {
l.allowLan = *allowLan
}
var socksErr, httpErr error
if allowLan != nil || httpPort != nil {
newHTTPPort := l.httpPort
if httpPort != nil {
newHTTPPort = *httpPort
}
httpErr = l.updateHTTP(newHTTPPort)
}
if allowLan != nil || socksPort != nil {
newSocksPort := l.socksPort
if socksPort != nil {
newSocksPort = *socksPort
}
socksErr = l.updateSocks(newSocksPort)
}
if socksErr != nil && httpErr != nil {
return fmt.Errorf("%s\n%s", socksErr.Error(), httpErr.Error())
} else if socksErr != nil {
return socksErr
} else if httpErr != nil {
return httpErr
} else {
return nil
}
}
func (l *Listener) updateHTTP(port int) error {
if l.httpSignal != nil {
signal := l.httpSignal
signal.Done <- struct{}{}
<-signal.Closed
l.httpSignal = nil
}
signal, err := http.NewHttpProxy(l.genAddr(port))
if err != nil {
return err
}
l.httpSignal = signal
l.httpPort = port
return nil
}
func (l *Listener) updateSocks(port int) error {
if l.socksSignal != nil {
signal := l.socksSignal
signal.Done <- struct{}{}
<-signal.Closed
l.socksSignal = nil
}
signal, err := socks.NewSocksProxy(l.genAddr(port))
if err != nil {
return err
}
l.socksSignal = signal
l.socksPort = port
return nil
}
func (l *Listener) genAddr(port int) string {
host := "127.0.0.1"
if l.allowLan {
host = ""
}
return fmt.Sprintf("%s:%d", host, port)
}
func (l *Listener) Run() error {
return l.Update(&l.allowLan, &l.httpPort, &l.socksPort)
}
func newListener() *Listener {
cfg, err := C.GetConfig()
if err != nil {
log.Fatalf("Read config error: %s", err.Error())
}
general := cfg.Section("General")
port := general.Key("port").RangeInt(C.DefalutHTTPPort, 1, 65535)
socksPort := general.Key("socks-port").RangeInt(C.DefalutSOCKSPort, 1, 65535)
allowLan := general.Key("allow-lan").MustBool()
return &Listener{
httpPort: port,
socksPort: socksPort,
allowLan: allowLan,
}
}
func Instance() *Listener {
once.Do(func() {
listener = newListener()
})
return listener
}

View File

@ -1,7 +1,6 @@
package socks
import (
"fmt"
"io"
"net"
"strconv"
@ -17,20 +16,41 @@ var (
tun = tunnel.GetInstance()
)
func NewSocksProxy(port string) {
l, err := net.Listen("tcp", fmt.Sprintf(":%s", port))
defer l.Close()
func NewSocksProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return
return nil, err
}
log.Infof("SOCKS proxy :%s", port)
for {
c, err := l.Accept()
if err != nil {
continue
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() {
log.Infof("SOCKS proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleSocks(c)
}
go handleSocks(c)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleSocks(conn net.Conn) {

View File

@ -27,6 +27,10 @@ func (dk *DomainKeyword) Adapter() string {
return dk.adapter
}
func (dk *DomainKeyword) Payload() string {
return dk.keyword
}
func NewDomainKeyword(keyword string, adapter string) *DomainKeyword {
return &DomainKeyword{
keyword: keyword,

View File

@ -27,6 +27,10 @@ func (ds *DomainSuffix) Adapter() string {
return ds.adapter
}
func (ds *DomainSuffix) Payload() string {
return ds.suffix
}
func NewDomainSuffix(suffix string, adapter string) *DomainSuffix {
return &DomainSuffix{
suffix: suffix,

View File

@ -20,6 +20,10 @@ func (f *Final) Adapter() string {
return f.adapter
}
func (f *Final) Payload() string {
return ""
}
func NewFinal(adapter string) *Final {
return &Final{
adapter: adapter,

View File

@ -38,6 +38,10 @@ func (g *GEOIP) Adapter() string {
return g.adapter
}
func (g *GEOIP) Payload() string {
return g.country
}
func NewGEOIP(country string, adapter string) *GEOIP {
return &GEOIP{
country: country,

View File

@ -23,8 +23,12 @@ func (i *IPCIDR) IsMatch(addr *C.Addr) bool {
return i.ipnet.Contains(*addr.IP)
}
func (g *IPCIDR) Adapter() string {
return g.adapter
func (i *IPCIDR) Adapter() string {
return i.adapter
}
func (i *IPCIDR) Payload() string {
return i.ipnet.String()
}
func NewIPCIDR(s string, adapter string) *IPCIDR {

View File

@ -7,21 +7,21 @@ import (
)
const (
INFO LogType = iota
ERROR LogLevel = iota
WARNING
ERROR
INFO
DEBUG
)
type LogType int
type LogLevel int
type Log struct {
LogType LogType
Payload string
LogLevel LogLevel
Payload string
}
func (l *Log) Type() string {
switch l.LogType {
switch l.LogLevel {
case INFO:
return "Info"
case WARNING:
@ -36,7 +36,7 @@ func (l *Log) Type() string {
}
func print(data Log) {
switch data.LogType {
switch data.LogLevel {
case INFO:
log.Infoln(data.Payload)
case WARNING:
@ -59,9 +59,9 @@ func (t *Tunnel) subscribeLogs() {
}
}
func newLog(logType LogType, format string, v ...interface{}) Log {
func newLog(logLevel LogLevel, format string, v ...interface{}) Log {
return Log{
LogType: logType,
Payload: fmt.Sprintf(format, v...),
LogLevel: logLevel,
Payload: fmt.Sprintf(format, v...),
}
}

22
tunnel/mode.go Normal file
View File

@ -0,0 +1,22 @@
package tunnel
type Mode int
const (
Global Mode = iota
Rule
Direct
)
func (m Mode) String() string {
switch m {
case Global:
return "Global"
case Rule:
return "Rule"
case Direct:
return "Direct"
default:
return "Unknow"
}
}

View File

@ -28,6 +28,8 @@ type Tunnel struct {
logCh chan interface{}
configLock *sync.RWMutex
traffic *C.Traffic
mode Mode
selector *adapters.Selector
}
func (t *Tunnel) Add(req C.ServerAdapter) {
@ -38,17 +40,29 @@ func (t *Tunnel) Traffic() *C.Traffic {
return t.traffic
}
func (t *Tunnel) Config() ([]C.Rule, map[string]C.Proxy) {
return t.rules, t.proxys
}
func (t *Tunnel) Log() *observable.Observable {
return t.observable
}
func (t *Tunnel) SetMode(mode Mode) {
t.mode = mode
}
func (t *Tunnel) GetMode() Mode {
return t.mode
}
func (t *Tunnel) UpdateConfig() (err error) {
cfg, err := C.GetConfig()
if err != nil {
return
}
// clear proxys and rules
// empty proxys and rules
proxys := make(map[string]C.Proxy)
rules := []C.Rule{}
@ -58,11 +72,10 @@ func (t *Tunnel) UpdateConfig() (err error) {
// parse proxy
for _, key := range proxysConfig.Keys() {
proxy := strings.Split(key.Value(), ",")
proxy := key.Strings(",")
if len(proxy) == 0 {
continue
}
proxy = trimArr(proxy)
switch proxy[0] {
// ss, server, port, cipter, password
case "ss":
@ -78,10 +91,6 @@ func (t *Tunnel) UpdateConfig() (err error) {
}
}
// init proxy
proxys["DIRECT"] = adapters.NewDirect(t.traffic)
proxys["REJECT"] = adapters.NewReject()
// parse rules
for _, key := range rulesConfig.Keys() {
rule := strings.Split(key.Name(), ",")
@ -106,12 +115,12 @@ func (t *Tunnel) UpdateConfig() (err error) {
// parse proxy groups
for _, key := range groupsConfig.Keys() {
rule := strings.Split(key.Value(), ",")
if len(rule) < 4 {
continue
}
rule = trimArr(rule)
switch rule[0] {
case "url-test":
if len(rule) < 4 {
return fmt.Errorf("URLTest need more than 4 param")
}
proxyNames := rule[1 : len(rule)-2]
delay, _ := strconv.Atoi(rule[len(rule)-1])
url := rule[len(rule)-2]
@ -127,14 +136,50 @@ func (t *Tunnel) UpdateConfig() (err error) {
return fmt.Errorf("Config error: %s", err.Error())
}
proxys[key.Name()] = adapter
case "select":
if len(rule) < 3 {
return fmt.Errorf("Selector need more than 3 param")
}
proxyNames := rule[1:]
selectProxy := make(map[string]C.Proxy)
for _, name := range proxyNames {
proxy, exist := proxys[name]
if !exist {
return fmt.Errorf("Proxy %s not exist", name)
}
selectProxy[name] = proxy
}
selector, err := adapters.NewSelector(key.Name(), selectProxy)
if err != nil {
return fmt.Errorf("Selector create error: %s", err.Error())
}
proxys[key.Name()] = selector
}
}
// init proxy
proxys["DIRECT"] = adapters.NewDirect(t.traffic)
proxys["REJECT"] = adapters.NewReject()
t.configLock.Lock()
defer t.configLock.Unlock()
// stop url-test
for _, elm := range t.proxys {
urlTest, ok := elm.(*adapters.URLTest)
if ok {
urlTest.Close()
}
}
s, err := adapters.NewSelector("Proxy", proxys)
if err != nil {
return err
}
t.proxys = proxys
t.rules = rules
t.selector = s
return nil
}
@ -151,7 +196,17 @@ func (t *Tunnel) process() {
func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
defer localConn.Close()
addr := localConn.Addr()
proxy := t.match(addr)
var proxy C.Proxy
switch t.mode {
case Direct:
proxy = t.proxys["DIRECT"]
case Global:
proxy = t.selector
// Rule
default:
proxy = t.match(addr)
}
remoConn, err := proxy.Generator(addr)
if err != nil {
t.logCh <- newLog(WARNING, "Proxy connect error: %s", err.Error())
@ -172,7 +227,7 @@ func (t *Tunnel) match(addr *C.Addr) C.Proxy {
if !ok {
continue
}
t.logCh <- newLog(INFO, "%v match %d using %s", addr.String(), rule.RuleType(), rule.Adapter())
t.logCh <- newLog(INFO, "%v match %s using %s", addr.String(), rule.RuleType().String(), rule.Adapter())
return a
}
}
@ -189,6 +244,7 @@ func newTunnel() *Tunnel {
logCh: logCh,
configLock: &sync.RWMutex{},
traffic: C.NewTraffic(time.Second),
mode: Rule,
}
go tunnel.process()
go tunnel.subscribeLogs()