mirror of https://github.com/ginuerzh/gost
14 changed files with 798 additions and 114 deletions
@ -0,0 +1,99 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"flag" |
||||
|
"fmt" |
||||
|
"io/ioutil" |
||||
|
"os" |
||||
|
"runtime" |
||||
|
|
||||
|
"github.com/ginuerzh/gost/gost" |
||||
|
"github.com/go-log/log" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
options struct { |
||||
|
chainNodes, serveNodes stringList |
||||
|
debugMode bool |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
var ( |
||||
|
configureFile string |
||||
|
printVersion bool |
||||
|
) |
||||
|
|
||||
|
flag.Var(&options.chainNodes, "F", "forward address, can make a forward chain") |
||||
|
flag.Var(&options.serveNodes, "L", "listen address, can listen on multiple ports") |
||||
|
flag.StringVar(&configureFile, "C", "", "configure file") |
||||
|
flag.BoolVar(&options.debugMode, "D", false, "enable debug log") |
||||
|
flag.BoolVar(&printVersion, "V", false, "print version") |
||||
|
flag.Parse() |
||||
|
|
||||
|
if err := loadConfigureFile(configureFile); err != nil { |
||||
|
log.Log(err) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
|
||||
|
if flag.NFlag() == 0 { |
||||
|
flag.PrintDefaults() |
||||
|
os.Exit(0) |
||||
|
} |
||||
|
|
||||
|
if printVersion { |
||||
|
fmt.Fprintf(os.Stderr, "gost %s (%s)\n", gost.Version, runtime.Version()) |
||||
|
os.Exit(0) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
func buildChain() (*gost.Chain, error) { |
||||
|
chain := gost.NewChain() |
||||
|
for _, cn := range options.chainNodes { |
||||
|
node, err := parseNode(cn) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
var tr gost.Transporter |
||||
|
switch node.Transport { |
||||
|
case "tls": |
||||
|
tr = gost.TLSTransporter() |
||||
|
case "ws": |
||||
|
tr = gost.WSTransporter(nil) |
||||
|
} |
||||
|
|
||||
|
var connector gost.Connector |
||||
|
} |
||||
|
|
||||
|
return chain, nil |
||||
|
} |
||||
|
|
||||
|
func loadConfigureFile(configureFile string) error { |
||||
|
if configureFile == "" { |
||||
|
return nil |
||||
|
} |
||||
|
content, err := ioutil.ReadFile(configureFile) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if err := json.Unmarshal(content, &options); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
type stringList []string |
||||
|
|
||||
|
func (l *stringList) String() string { |
||||
|
return fmt.Sprintf("%s", *l) |
||||
|
} |
||||
|
func (l *stringList) Set(value string) error { |
||||
|
*l = append(*l, value) |
||||
|
return nil |
||||
|
} |
||||
@ -0,0 +1,139 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"bufio" |
||||
|
"net" |
||||
|
"net/url" |
||||
|
"os" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/ginuerzh/gost/gost" |
||||
|
"github.com/go-log/log" |
||||
|
) |
||||
|
|
||||
|
type node struct { |
||||
|
Addr string |
||||
|
Protocol string // protocol: http/socks5/ss
|
||||
|
Transport string // transport: ws/wss/tls/http2/tcp/udp/rtcp/rudp
|
||||
|
Remote string // remote address, used by tcp/udp port forwarding
|
||||
|
Users []*url.Userinfo // authentication for proxy
|
||||
|
Whitelist *gost.Permissions |
||||
|
Blacklist *gost.Permissions |
||||
|
values url.Values |
||||
|
serverName string |
||||
|
} |
||||
|
|
||||
|
func parseNode(s string) (n node, err error) { |
||||
|
if !strings.Contains(s, "://") { |
||||
|
s = "gost://" + s |
||||
|
} |
||||
|
u, err := url.Parse(s) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
query := u.Query() |
||||
|
|
||||
|
n = node{ |
||||
|
Addr: u.Host, |
||||
|
} |
||||
|
|
||||
|
if query.Get("whitelist") != "" { |
||||
|
if n.Whitelist, err = gost.ParsePermissions(query.Get("whitelist")); err != nil { |
||||
|
return |
||||
|
} |
||||
|
} else { |
||||
|
// By default allow for everyting
|
||||
|
n.Whitelist, _ = gost.ParsePermissions("*:*:*") |
||||
|
} |
||||
|
|
||||
|
if query.Get("blacklist") != "" { |
||||
|
if n.Blacklist, err = gost.ParsePermissions(query.Get("blacklist")); err != nil { |
||||
|
return |
||||
|
} |
||||
|
} else { |
||||
|
// By default block nothing
|
||||
|
n.Blacklist, _ = gost.ParsePermissions("") |
||||
|
} |
||||
|
|
||||
|
if u.User != nil { |
||||
|
n.Users = append(n.Users, u.User) |
||||
|
} |
||||
|
|
||||
|
users, er := parseUsers(n.values.Get("secrets")) |
||||
|
if users != nil { |
||||
|
n.Users = append(n.Users, users...) |
||||
|
} |
||||
|
if er != nil { |
||||
|
log.Log("load secrets:", er) |
||||
|
} |
||||
|
|
||||
|
if strings.Contains(u.Host, ":") { |
||||
|
n.serverName, _, _ = net.SplitHostPort(u.Host) |
||||
|
if n.serverName == "" { |
||||
|
n.serverName = "localhost" // default server name
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
schemes := strings.Split(u.Scheme, "+") |
||||
|
if len(schemes) == 1 { |
||||
|
n.Protocol = schemes[0] |
||||
|
n.Transport = schemes[0] |
||||
|
} |
||||
|
if len(schemes) == 2 { |
||||
|
n.Protocol = schemes[0] |
||||
|
n.Transport = schemes[1] |
||||
|
} |
||||
|
|
||||
|
switch n.Transport { |
||||
|
case "ws", "wss", "tls", "h2", "h2c", "quic", "kcp", "redirect", "ssu", "ssh": |
||||
|
case "https": |
||||
|
n.Protocol = "http" |
||||
|
n.Transport = "tls" |
||||
|
case "http2": // http2 -> http2+tls, h2c mode is http2+tcp
|
||||
|
n.Protocol = "http2" |
||||
|
n.Transport = "tls" |
||||
|
case "tcp", "udp": // started from v2.1, tcp and udp are for local port forwarding
|
||||
|
n.Remote = strings.Trim(u.EscapedPath(), "/") |
||||
|
case "rtcp", "rudp": // rtcp and rudp are for remote port forwarding
|
||||
|
n.Remote = strings.Trim(u.EscapedPath(), "/") |
||||
|
default: |
||||
|
n.Transport = "" |
||||
|
} |
||||
|
|
||||
|
switch n.Protocol { |
||||
|
case "http", "http2", "socks", "socks4", "socks4a", "socks5", "ss": |
||||
|
default: |
||||
|
n.Protocol = "" |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func parseUsers(s string) (users []*url.Userinfo, err error) { |
||||
|
if s == "" { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
f, err := os.Open(s) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
scanner := bufio.NewScanner(f) |
||||
|
for scanner.Scan() { |
||||
|
line := strings.TrimSpace(scanner.Text()) |
||||
|
if line == "" || strings.HasPrefix(line, "#") { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
s := strings.SplitN(line, " ", 2) |
||||
|
if len(s) == 1 { |
||||
|
users = append(users, url.User(strings.TrimSpace(s[0]))) |
||||
|
} else if len(s) == 2 { |
||||
|
users = append(users, url.UserPassword(strings.TrimSpace(s[0]), strings.TrimSpace(s[1]))) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
err = scanner.Err() |
||||
|
return |
||||
|
} |
||||
@ -1,18 +1,29 @@ |
|||||
package gost |
package gost |
||||
|
|
||||
import "log" |
import ( |
||||
|
"log" |
||||
|
) |
||||
|
|
||||
func init() { |
func init() { |
||||
log.SetFlags(log.LstdFlags | log.Lshortfile) |
log.SetFlags(log.LstdFlags | log.Lshortfile) |
||||
} |
} |
||||
|
|
||||
type logger struct { |
type LogLogger struct { |
||||
} |
} |
||||
|
|
||||
func (l *logger) Log(v ...interface{}) { |
func (l *LogLogger) Log(v ...interface{}) { |
||||
log.Println(v...) |
log.Println(v...) |
||||
} |
} |
||||
|
|
||||
func (l *logger) Logf(format string, v ...interface{}) { |
func (l *LogLogger) Logf(format string, v ...interface{}) { |
||||
log.Printf(format, v...) |
log.Printf(format, v...) |
||||
} |
} |
||||
|
|
||||
|
type NopLogger struct { |
||||
|
} |
||||
|
|
||||
|
func (l *NopLogger) Log(v ...interface{}) { |
||||
|
} |
||||
|
|
||||
|
func (l *NopLogger) Logf(format string, v ...interface{}) { |
||||
|
} |
||||
|
|||||
@ -0,0 +1,185 @@ |
|||||
|
package gost |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
|
||||
|
glob "github.com/ryanuber/go-glob" |
||||
|
) |
||||
|
|
||||
|
type PortRange struct { |
||||
|
Min, Max int |
||||
|
} |
||||
|
|
||||
|
type PortSet []PortRange |
||||
|
|
||||
|
type StringSet []string |
||||
|
|
||||
|
type Permission struct { |
||||
|
Actions StringSet |
||||
|
Hosts StringSet |
||||
|
Ports PortSet |
||||
|
} |
||||
|
|
||||
|
type Permissions []Permission |
||||
|
|
||||
|
func minint(x, y int) int { |
||||
|
if x < y { |
||||
|
return x |
||||
|
} |
||||
|
return y |
||||
|
} |
||||
|
|
||||
|
func maxint(x, y int) int { |
||||
|
if x > y { |
||||
|
return x |
||||
|
} |
||||
|
return y |
||||
|
} |
||||
|
|
||||
|
func (ir *PortRange) Contains(value int) bool { |
||||
|
return value >= ir.Min && value <= ir.Max |
||||
|
} |
||||
|
|
||||
|
func ParsePortRange(s string) (*PortRange, error) { |
||||
|
if s == "*" { |
||||
|
return &PortRange{Min: 0, Max: 65535}, nil |
||||
|
} |
||||
|
|
||||
|
minmax := strings.Split(s, "-") |
||||
|
switch len(minmax) { |
||||
|
case 1: |
||||
|
port, err := strconv.Atoi(s) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
if port < 0 || port > 65535 { |
||||
|
return nil, fmt.Errorf("invalid port: %s", s) |
||||
|
} |
||||
|
return &PortRange{Min: port, Max: port}, nil |
||||
|
case 2: |
||||
|
min, err := strconv.Atoi(minmax[0]) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
max, err := strconv.Atoi(minmax[1]) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
realmin := maxint(0, minint(min, max)) |
||||
|
realmax := minint(65535, maxint(min, max)) |
||||
|
|
||||
|
return &PortRange{Min: realmin, Max: realmax}, nil |
||||
|
default: |
||||
|
return nil, fmt.Errorf("invalid range: %s", s) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (ps *PortSet) Contains(value int) bool { |
||||
|
for _, portRange := range *ps { |
||||
|
if portRange.Contains(value) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func ParsePortSet(s string) (*PortSet, error) { |
||||
|
ps := &PortSet{} |
||||
|
|
||||
|
if s == "" { |
||||
|
return nil, errors.New("must specify at least one port") |
||||
|
} |
||||
|
|
||||
|
ranges := strings.Split(s, ",") |
||||
|
|
||||
|
for _, r := range ranges { |
||||
|
portRange, err := ParsePortRange(r) |
||||
|
|
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
*ps = append(*ps, *portRange) |
||||
|
} |
||||
|
|
||||
|
return ps, nil |
||||
|
} |
||||
|
|
||||
|
func (ss *StringSet) Contains(subj string) bool { |
||||
|
for _, s := range *ss { |
||||
|
if glob.Glob(s, subj) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func ParseStringSet(s string) (*StringSet, error) { |
||||
|
ss := &StringSet{} |
||||
|
if s == "" { |
||||
|
return nil, errors.New("cannot be empty") |
||||
|
} |
||||
|
|
||||
|
*ss = strings.Split(s, ",") |
||||
|
|
||||
|
return ss, nil |
||||
|
} |
||||
|
|
||||
|
func (ps *Permissions) Can(action string, host string, port int) bool { |
||||
|
for _, p := range *ps { |
||||
|
if p.Actions.Contains(action) && p.Hosts.Contains(host) && p.Ports.Contains(port) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func ParsePermissions(s string) (*Permissions, error) { |
||||
|
ps := &Permissions{} |
||||
|
|
||||
|
if s == "" { |
||||
|
return &Permissions{}, nil |
||||
|
} |
||||
|
|
||||
|
perms := strings.Split(s, " ") |
||||
|
|
||||
|
for _, perm := range perms { |
||||
|
parts := strings.Split(perm, ":") |
||||
|
|
||||
|
switch len(parts) { |
||||
|
case 3: |
||||
|
actions, err := ParseStringSet(parts[0]) |
||||
|
|
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("action list must look like connect,bind given: %s", parts[0]) |
||||
|
} |
||||
|
|
||||
|
hosts, err := ParseStringSet(parts[1]) |
||||
|
|
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("hosts list must look like google.pl,*.google.com given: %s", parts[1]) |
||||
|
} |
||||
|
|
||||
|
ports, err := ParsePortSet(parts[2]) |
||||
|
|
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("ports list must look like 80,8000-9000, given: %s", parts[2]) |
||||
|
} |
||||
|
|
||||
|
permission := Permission{Actions: *actions, Hosts: *hosts, Ports: *ports} |
||||
|
|
||||
|
*ps = append(*ps, permission) |
||||
|
default: |
||||
|
return nil, fmt.Errorf("permission must have format [actions]:[hosts]:[ports] given: %s", perm) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ps, nil |
||||
|
} |
||||
@ -0,0 +1,152 @@ |
|||||
|
package gost |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"testing" |
||||
|
) |
||||
|
|
||||
|
var portRangeTests = []struct { |
||||
|
in string |
||||
|
out *PortRange |
||||
|
}{ |
||||
|
{"1", &PortRange{Min: 1, Max: 1}}, |
||||
|
{"1-3", &PortRange{Min: 1, Max: 3}}, |
||||
|
{"3-1", &PortRange{Min: 1, Max: 3}}, |
||||
|
{"0-100000", &PortRange{Min: 0, Max: 65535}}, |
||||
|
{"*", &PortRange{Min: 0, Max: 65535}}, |
||||
|
} |
||||
|
|
||||
|
var stringSetTests = []struct { |
||||
|
in string |
||||
|
out *StringSet |
||||
|
}{ |
||||
|
{"*", &StringSet{"*"}}, |
||||
|
{"google.pl,google.com", &StringSet{"google.pl", "google.com"}}, |
||||
|
} |
||||
|
|
||||
|
var portSetTests = []struct { |
||||
|
in string |
||||
|
out *PortSet |
||||
|
}{ |
||||
|
{"1,3", &PortSet{PortRange{Min: 1, Max: 1}, PortRange{Min: 3, Max: 3}}}, |
||||
|
{"1-3,7-5", &PortSet{PortRange{Min: 1, Max: 3}, PortRange{Min: 5, Max: 7}}}, |
||||
|
{"0-100000", &PortSet{PortRange{Min: 0, Max: 65535}}}, |
||||
|
{"*", &PortSet{PortRange{Min: 0, Max: 65535}}}, |
||||
|
} |
||||
|
|
||||
|
var permissionsTests = []struct { |
||||
|
in string |
||||
|
out *Permissions |
||||
|
}{ |
||||
|
{"", &Permissions{}}, |
||||
|
{"*:*:*", &Permissions{ |
||||
|
Permission{ |
||||
|
Actions: StringSet{"*"}, |
||||
|
Hosts: StringSet{"*"}, |
||||
|
Ports: PortSet{PortRange{Min: 0, Max: 65535}}, |
||||
|
}, |
||||
|
}}, |
||||
|
{"bind:127.0.0.1,localhost:80,443,8000-8100 connect:*.google.pl:80,443", &Permissions{ |
||||
|
Permission{ |
||||
|
Actions: StringSet{"bind"}, |
||||
|
Hosts: StringSet{"127.0.0.1", "localhost"}, |
||||
|
Ports: PortSet{ |
||||
|
PortRange{Min: 80, Max: 80}, |
||||
|
PortRange{Min: 443, Max: 443}, |
||||
|
PortRange{Min: 8000, Max: 8100}, |
||||
|
}, |
||||
|
}, |
||||
|
Permission{ |
||||
|
Actions: StringSet{"connect"}, |
||||
|
Hosts: StringSet{"*.google.pl"}, |
||||
|
Ports: PortSet{ |
||||
|
PortRange{Min: 80, Max: 80}, |
||||
|
PortRange{Min: 443, Max: 443}, |
||||
|
}, |
||||
|
}, |
||||
|
}}, |
||||
|
} |
||||
|
|
||||
|
func TestPortRangeParse(t *testing.T) { |
||||
|
for _, test := range portRangeTests { |
||||
|
actual, err := ParsePortRange(test.in) |
||||
|
if err != nil { |
||||
|
t.Errorf("ParsePortRange(%q) returned error: %v", test.in, err) |
||||
|
} else if *actual != *test.out { |
||||
|
t.Errorf("ParsePortRange(%q): got %v, want %v", test.in, actual, test.out) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestPortRangeContains(t *testing.T) { |
||||
|
actual, _ := ParsePortRange("5-10") |
||||
|
|
||||
|
if !actual.Contains(5) || !actual.Contains(7) || !actual.Contains(10) { |
||||
|
t.Errorf("5-10 should contain 5, 7 and 10") |
||||
|
} |
||||
|
|
||||
|
if actual.Contains(4) || actual.Contains(11) { |
||||
|
t.Errorf("5-10 should not contain 4, 11") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestStringSetParse(t *testing.T) { |
||||
|
for _, test := range stringSetTests { |
||||
|
actual, err := ParseStringSet(test.in) |
||||
|
if err != nil { |
||||
|
t.Errorf("ParseStringSet(%q) returned error: %v", test.in, err) |
||||
|
} else if fmt.Sprintln(actual) != fmt.Sprintln(test.out) { |
||||
|
t.Errorf("ParseStringSet(%q): got %v, want %v", test.in, actual, test.out) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestStringSetContains(t *testing.T) { |
||||
|
ss, _ := ParseStringSet("google.pl,*.google.com") |
||||
|
|
||||
|
if !ss.Contains("google.pl") || !ss.Contains("www.google.com") { |
||||
|
t.Errorf("google.pl,*.google.com should contain google.pl and www.google.com") |
||||
|
} |
||||
|
|
||||
|
if ss.Contains("www.google.pl") || ss.Contains("foobar.com") { |
||||
|
t.Errorf("google.pl,*.google.com shound not contain www.google.pl and foobar.com") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestPortSetParse(t *testing.T) { |
||||
|
for _, test := range portSetTests { |
||||
|
actual, err := ParsePortSet(test.in) |
||||
|
if err != nil { |
||||
|
t.Errorf("ParsePortRange(%q) returned error: %v", test.in, err) |
||||
|
} else if fmt.Sprintln(actual) != fmt.Sprintln(test.out) { |
||||
|
t.Errorf("ParsePortRange(%q): got %v, want %v", test.in, actual, test.out) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestPortSetContains(t *testing.T) { |
||||
|
actual, _ := ParsePortSet("5-10,20-30") |
||||
|
|
||||
|
if !actual.Contains(5) || !actual.Contains(7) || !actual.Contains(10) { |
||||
|
t.Errorf("5-10,20-30 should contain 5, 7 and 10") |
||||
|
} |
||||
|
|
||||
|
if !actual.Contains(20) || !actual.Contains(27) || !actual.Contains(30) { |
||||
|
t.Errorf("5-10,20-30 should contain 20, 27 and 30") |
||||
|
} |
||||
|
|
||||
|
if actual.Contains(4) || actual.Contains(11) || actual.Contains(31) { |
||||
|
t.Errorf("5-10,20-30 should not contain 4, 11, 31") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestPermissionsParse(t *testing.T) { |
||||
|
for _, test := range permissionsTests { |
||||
|
actual, err := ParsePermissions(test.in) |
||||
|
if err != nil { |
||||
|
t.Errorf("ParsePermissions(%q) returned error: %v", test.in, err) |
||||
|
} else if fmt.Sprintln(actual) != fmt.Sprintln(test.out) { |
||||
|
t.Errorf("ParsePermissions(%q): got %v, want %v", test.in, actual, test.out) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue