mirror of
https://github.com/zhigang1992/flora-kit.git
synced 2026-01-12 17:22:39 +08:00
v0.2.5
- windows 透明代理
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -29,3 +29,5 @@ flora-kit
|
||||
*.log
|
||||
release/
|
||||
*.tar.*
|
||||
.idea
|
||||
dist/
|
||||
|
||||
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
||||
0.2.3
|
||||
-----
|
||||
- 支持```[General]skip-Proxy```
|
||||
- 重构版,方便其他代理模块加入
|
||||
|
||||
0.2.2
|
||||
-----
|
||||
|
||||
- 修正打包确缺失默认配置文件的问题。
|
||||
37
Makefile
37
Makefile
@@ -1,30 +1,39 @@
|
||||
RELEASE_PATH = release/flora/
|
||||
RELEASE_PATH = release
|
||||
PACKAGE_PATH = release/flora
|
||||
|
||||
install:
|
||||
@go get
|
||||
build:
|
||||
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o release/flora-kit-darwin-amd64
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o release/flora-kit-linux-amd64
|
||||
GOOS=linux GOARCH=386 go build -ldflags "-s -w" -o release/flora-kit-linux-386
|
||||
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-darwin-amd64
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-amd64
|
||||
GOOS=linux GOARCH=386 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-386
|
||||
GOOS=windows GOARCH=386 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-386.exe
|
||||
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-amd64.exe
|
||||
package:
|
||||
rm -Rf $(RELEASE_PATH)/*
|
||||
mkdir -p $(RELEASE_PATH)
|
||||
cp ./flora.default.conf $(RELEASE_PATH)
|
||||
cp ./geoip.mmdb $(RELEASE_PATH)
|
||||
rm -Rf $(PACKAGE_PATH)/*
|
||||
mkdir -p $(PACKAGE_PATH)
|
||||
cp ./flora.default.conf $(PACKAGE_PATH)
|
||||
cp ./geoip.mmdb $(PACKAGE_PATH)
|
||||
cp ./LICENSE $(RELEASE_PATH)
|
||||
cp ./README.md $(RELEASE_PATH)
|
||||
cp ./README.md $(PACKAGE_PATH)
|
||||
# macOS
|
||||
cp ./release/flora-kit-darwin-amd64 $(RELEASE_PATH)flora
|
||||
cp ./release/flora-darwin-amd64 $(PACKAGE_PATH)
|
||||
cd ./release && zip flora-darwin-amd64.zip flora
|
||||
# Linux amd64
|
||||
cp ./release/flora-kit-linux-amd64 $(RELEASE_PATH)flora
|
||||
cp ./release/flora-amd64 $(PACKAGE_PATH)flora
|
||||
cd ./release && tar zcf flora-linux-amd64.tar.gz flora
|
||||
# Linux 386
|
||||
cp ./release/flora-kit-linux-386 $(RELEASE_PATH)flora
|
||||
cp ./release/flora-386 $(PACKAGE_PATH)
|
||||
cd ./release && tar zcf flora-linux-386.tar.gz flora
|
||||
# Windows 386
|
||||
#cp ./release/flora-386.exe $(PACKAGE_PATH)
|
||||
#cd ./release && tar zcf flora-win-386.tar.gz flora
|
||||
# Windows amd64
|
||||
#cp ./release/flora-amd64.exe $(PACKAGE_PATH)
|
||||
#cd ./release && tar zcf flora-win-amd64.tar.gz flora
|
||||
# remove history
|
||||
rm $(RELEASE_PATH)flora
|
||||
rm $(PACKAGE_PATH)flora
|
||||
run:
|
||||
@go run main.go
|
||||
test:
|
||||
@go test ./flora
|
||||
@go test ./flora
|
||||
|
||||
21
README.md
21
README.md
@@ -14,7 +14,8 @@ Flora
|
||||
- 支持域名关键词、前缀、后缀匹配,制定 Direct 访问(白名单)或用 Proxy 访问(黑名单);
|
||||
- 支持 IP 白名单,黑名单;
|
||||
- 支持 GeoIP 判断目标网站服务器所在区域,自动选择线路;
|
||||
- 启动的时候自动改变 macOS 网路代理配置,无需手工调整;
|
||||
- 启动的时候自动改变 macOS,windows 网路代理配置,无需手工调整;
|
||||
|
||||
|
||||
## TODO
|
||||
|
||||
@@ -30,9 +31,21 @@ https://github.com/huacnlee/flora-kit/releases
|
||||
|
||||
> NOTE: 由于启动的时候,需要修改系统的网络配置,所以你需要用 sudo 来执行:
|
||||
|
||||
```bash
|
||||
cd flora
|
||||
sudo ./flora
|
||||
#### macOS
|
||||
```
|
||||
$ cd flora
|
||||
$ sudo ./flora
|
||||
```
|
||||
|
||||
#### Linux
|
||||
```
|
||||
$ cd flora
|
||||
$ ./flora
|
||||
```
|
||||
|
||||
#### Windows
|
||||
```
|
||||
flora.exe
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
[General]
|
||||
loglevel = notify
|
||||
skip-proxy = 127.0.0.1, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, localhost, *.local, ::ffff:0:0:0:0/1, ::ffff:128:0:0:0/1
|
||||
bypass-tun = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12
|
||||
replica = false
|
||||
skip-Proxy = 127.0.0.1, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, localhost, *.local, e.crashlytics.com
|
||||
|
||||
# dns-server = 119.29.29.29,223.5.5.5,114.114.115.115
|
||||
# external-controller-access = PASSWORD@0.0.0.0:6155
|
||||
# ipv6 = true
|
||||
# 以下参数仅供 iOS 版本使用
|
||||
bypass-system = true
|
||||
bypass-tun = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 0.0.0.0/31
|
||||
ipv6 = true
|
||||
|
||||
# 以下参数仅供 macOS 版本使用
|
||||
interface = 127.0.0.1
|
||||
socks-port = 1080
|
||||
|
||||
allow-wifi-access = true
|
||||
enhanced-mode-by-rule = true
|
||||
exclude-simple-hostnames = true
|
||||
|
||||
[Proxy]
|
||||
DIRECT = direct
|
||||
# Proxy = shadowsocks, server-ip, port, method, password
|
||||
|
||||
# [SSID Setting]
|
||||
# WiFi-SSID-NAME suspend=true
|
||||
|
||||
[URL Rewrite]
|
||||
# 302 REDIRECT
|
||||
[Proxy Group]
|
||||
# 🚀 Proxy = select, 🌞 Line
|
||||
|
||||
[Rule]
|
||||
// BLOCK ADS
|
||||
# DOMAIN-SUFFIX,mgid.com,REJECT
|
||||
|
||||
// DIRECT RULES
|
||||
# DOMAIN-SUFFIX,cn,DIRECT
|
||||
|
||||
// NORMAL RULES
|
||||
# DOMAIN-KEYWORD,instagram,Proxy
|
||||
# DOMAIN-SUFFIX,youtube.com,Proxy
|
||||
|
||||
# China IP use DIRECT
|
||||
// China IP use DIRECT
|
||||
GEOIP,CN,DIRECT
|
||||
# Other
|
||||
FINAL,Proxy
|
||||
FINAL,Proxy
|
||||
|
||||
396
flora/config.go
396
flora/config.go
@@ -9,70 +9,73 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
const (
|
||||
VERSION = "0.1.1"
|
||||
SOCKS_PORT = 7657
|
||||
RULE_REJECT = 0
|
||||
RULE_DIRECT = 1
|
||||
RULE_PROXY = 2
|
||||
DEFAULT_SOCKS_PORT = 1080
|
||||
)
|
||||
|
||||
type DomainRule struct {
|
||||
S string
|
||||
T int
|
||||
type ProxyConfig struct {
|
||||
SurgeConfig *ini.File
|
||||
GeoDbPath string
|
||||
LocalSocksPort int
|
||||
LocalHost string
|
||||
proxyServer map[string]ProxyServer
|
||||
proxyGroup map[string]*proxyGroup
|
||||
|
||||
bypassDomains []interface{}
|
||||
systemBypass []string
|
||||
ruleSuffixDomains []*Rule
|
||||
rulePrefixDomains []*Rule
|
||||
ruleKeywordDomains []*Rule
|
||||
ruleUserAgent []*Rule
|
||||
ruleGeoIP []*Rule
|
||||
ruleFinal *Rule
|
||||
}
|
||||
type proxyGroup struct {
|
||||
mode string
|
||||
proxyServers []ProxyServer
|
||||
}
|
||||
|
||||
var ruleSuffixDomains = []*DomainRule{}
|
||||
var rulePrefixDomains = []*DomainRule{}
|
||||
var ruleKeywordDomains = []*DomainRule{}
|
||||
var ruleGeoIP = &DomainRule{}
|
||||
|
||||
var ssConfig ss.Config
|
||||
var iniConfig *ini.File
|
||||
var debug ss.DebugLog
|
||||
|
||||
type ProxyServerCipher struct {
|
||||
Server string
|
||||
Cipher *ss.Cipher
|
||||
}
|
||||
|
||||
var ProxyServers struct {
|
||||
SrvCipher []*ProxyServerCipher
|
||||
FailCnt []int // failed connection count
|
||||
}
|
||||
|
||||
func init() {
|
||||
var configFilename = "./flora.default.conf"
|
||||
var userConfigFilename = "./flora.user.conf"
|
||||
func LoadConfig(cfgFile string, geoFile string) (*ProxyConfig) {
|
||||
proxyConfig := ProxyConfig{}
|
||||
var iniOpts = ini.LoadOptions{
|
||||
AllowBooleanKeys: true,
|
||||
Loose: true,
|
||||
Insensitive: true,
|
||||
}
|
||||
cfg, err := ini.LoadSources(iniOpts, configFilename, userConfigFilename)
|
||||
sep := string(os.PathSeparator)
|
||||
pwd, _ := os.Getwd()
|
||||
var geoFilename = geoFile
|
||||
var err error
|
||||
var defaultCfgName = strings.Join([]string{pwd, "flora.default.conf"}, sep)
|
||||
var userConfigFilename = strings.Join([]string{pwd, "flora.user.conf"}, sep)
|
||||
var speCfgName string
|
||||
if _, err := os.Stat(cfgFile); nil != err && os.IsNotExist(err) {
|
||||
speCfgName = strings.Join([]string{pwd, cfgFile}, sep)
|
||||
}
|
||||
if _, err := os.Stat(geoFilename); nil != err && os.IsNotExist(err) {
|
||||
geoFilename = strings.Join([]string{pwd, "geoip.mmdb"}, sep)
|
||||
}
|
||||
|
||||
proxyConfig.SurgeConfig, err = ini.LoadSources(iniOpts, speCfgName, defaultCfgName, userConfigFilename)
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Config file %v not found, or have error: \n\t%v", configFilename, err))
|
||||
panic(fmt.Sprintf("Config file %v not found, or have error: \n\t%v", cfgFile, err))
|
||||
}
|
||||
iniConfig = cfg
|
||||
loadGeneral(&proxyConfig)
|
||||
loadProxy(&proxyConfig)
|
||||
loadProxyGroup(&proxyConfig)
|
||||
loadGeoIP(geoFilename)
|
||||
loadRules(&proxyConfig)
|
||||
|
||||
loadGeoIP()
|
||||
loadGeneral()
|
||||
loadProxy()
|
||||
loadRules()
|
||||
|
||||
SetSocksFirewallProxy()
|
||||
|
||||
debug.Println("104.244.42.129", GeoIPString("104.244.42.129"))
|
||||
debug.Println(RuleOfHost("www.twitter.com"))
|
||||
return &proxyConfig
|
||||
}
|
||||
|
||||
// [General] section
|
||||
func loadGeneral() {
|
||||
section := iniConfig.Section("General")
|
||||
|
||||
func loadGeneral(config *ProxyConfig) {
|
||||
section := config.SurgeConfig.Section("General")
|
||||
bypassDomains := []string{}
|
||||
if section.HasKey("skip-proxy") {
|
||||
bypassDomains = append(bypassDomains, readArrayLine(section.Key("skip-proxy").String())...)
|
||||
@@ -80,74 +83,142 @@ func loadGeneral() {
|
||||
if section.HasKey("bypass-tun") {
|
||||
bypassDomains = append(bypassDomains, readArrayLine(section.Key("bypass-tun").String())...)
|
||||
}
|
||||
SetProxyBypassDomains(bypassDomains)
|
||||
config.LocalSocksPort = DEFAULT_SOCKS_PORT
|
||||
if section.HasKey("socks-port") {
|
||||
port, err := strconv.Atoi(section.Key("socks-port").String())
|
||||
if nil == err {
|
||||
config.LocalSocksPort = port
|
||||
}
|
||||
}
|
||||
config.LocalHost = "127.0.0.1"
|
||||
if section.Haskey("interface") {
|
||||
ipStr := section.Key("interface").String()
|
||||
addr := net.ParseIP(ipStr)
|
||||
if nil != addr {
|
||||
config.LocalHost = ipStr
|
||||
}
|
||||
}
|
||||
config.systemBypass = bypassDomains
|
||||
//load bypass
|
||||
config.bypassDomains = make([]interface{}, len(bypassDomains))
|
||||
for i, v := range bypassDomains {
|
||||
ip := net.ParseIP(v)
|
||||
if nil != ip {
|
||||
config.bypassDomains[i] = ip
|
||||
} else if _, n, err := net.ParseCIDR(v); err == nil {
|
||||
config.bypassDomains[i] = n
|
||||
} else {
|
||||
config.bypassDomains[i] = v
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// [Proxy] Section
|
||||
func loadProxy() {
|
||||
ssConfig.LocalPort = SOCKS_PORT
|
||||
for _, key := range iniConfig.Section("Proxy").Keys() {
|
||||
var proxys = readArrayLine(key.String())
|
||||
// ShadowSocks Proxys
|
||||
if proxys[0] == "custom" || proxys[0] == "shadowsocks" {
|
||||
var server = strings.Join(proxys[1:3], ":")
|
||||
var serverInfo = []string{server, proxys[4], proxys[3]}
|
||||
ssConfig.ServerPassword = append(ssConfig.ServerPassword, serverInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if ssConfig.Method == "" {
|
||||
ssConfig.Method = "aes-256-cfb"
|
||||
}
|
||||
if len(ssConfig.ServerPassword) == 0 {
|
||||
if !enoughSSOptions(&ssConfig) {
|
||||
fmt.Fprintln(os.Stderr, "must specify server address, password and both server/local port")
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
if ssConfig.LocalPort == 0 {
|
||||
fmt.Fprintln(os.Stderr, "must specify local port")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
parseServerConfig()
|
||||
}
|
||||
|
||||
// 载入 [Rule]
|
||||
func loadRules() {
|
||||
for _, key := range iniConfig.Section("Rule").KeyStrings() {
|
||||
var items = readArrayLine(key)
|
||||
var ruleType = RULE_DIRECT
|
||||
if len(items) >= 3 {
|
||||
switch items[2] {
|
||||
case "proxy":
|
||||
ruleType = RULE_PROXY
|
||||
case "direct":
|
||||
ruleType = RULE_DIRECT
|
||||
case "reject":
|
||||
ruleType = RULE_REJECT
|
||||
default:
|
||||
ruleType = RULE_DIRECT
|
||||
func loadProxy(config *ProxyConfig) {
|
||||
config.proxyServer = make(map[string]ProxyServer)
|
||||
section := config.SurgeConfig.Section("Proxy")
|
||||
for _, name := range section.KeyStrings() {
|
||||
proxyName := strings.ToLower(name)
|
||||
v, _ := section.GetKey(proxyName)
|
||||
var proxyStrCfg = readArrayLine(v.String())
|
||||
serverType := strings.ToLower(proxyStrCfg[0])
|
||||
var proxy ProxyServer
|
||||
if serverType == ServerTypeShadowSocks || serverType == ServerTypeCustom {
|
||||
//[ip:port,password,method]
|
||||
if len(proxyStrCfg) > 1 {
|
||||
c, err := ss.NewCipher(proxyStrCfg[3], proxyStrCfg[4])
|
||||
if nil != err {
|
||||
log.Printf("Loading shadowsocks proxy server %s has error ", proxyName)
|
||||
continue
|
||||
}
|
||||
proxy = NewShadowSocks(strings.Join(proxyStrCfg[1:3], ":"), c)
|
||||
}
|
||||
}
|
||||
|
||||
switch items[0] {
|
||||
case "domain-suffix":
|
||||
ruleSuffixDomains = append(ruleSuffixDomains, &DomainRule{S: items[1], T: ruleType})
|
||||
case "domain-prefix":
|
||||
rulePrefixDomains = append(rulePrefixDomains, &DomainRule{S: items[1], T: ruleType})
|
||||
case "domain-keyword":
|
||||
ruleKeywordDomains = append(ruleKeywordDomains, &DomainRule{S: items[1], T: ruleType})
|
||||
case "geoip":
|
||||
ruleGeoIP = &DomainRule{S: items[1], T: ruleType}
|
||||
} else if serverType == ServerTypeDirect {
|
||||
proxy = NewDirect()
|
||||
} else if serverType == ServerTypeReject {
|
||||
proxy = NewReject()
|
||||
}
|
||||
if nil != proxy {
|
||||
log.Printf("Loading proxy server %s done. ", proxyName)
|
||||
config.proxyServer[proxyName] = proxy
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func enoughSSOptions(config *ss.Config) bool {
|
||||
return config.Server != nil && config.ServerPort != 0 &&
|
||||
config.LocalPort != 0 && config.Password != ""
|
||||
func (c *ProxyConfig) GetProxyServer(action string) (ProxyServer) {
|
||||
const maxFailCnt = 30
|
||||
var server ProxyServer
|
||||
var ok bool
|
||||
server, ok = c.proxyServer[action]
|
||||
if !ok {
|
||||
group, ok := c.proxyGroup[action]
|
||||
if ok {
|
||||
for _, s := range group.proxyServers {
|
||||
eff := []ProxyServer{}
|
||||
if s.FailCount() < maxFailCnt {
|
||||
eff = append(eff, s)
|
||||
}
|
||||
l := len(eff)
|
||||
if l > 0 {
|
||||
return eff[rand.Intn(l)]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
server = NewDirect()
|
||||
}
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
//[Proxy Group] Section
|
||||
func loadProxyGroup(config *ProxyConfig) {
|
||||
section := config.SurgeConfig.Section("Proxy Group")
|
||||
config.proxyGroup = make(map[string]*proxyGroup)
|
||||
for _, name := range section.KeyStrings() {
|
||||
groupName := strings.ToLower(name)
|
||||
v, _ := section.GetKey(groupName)
|
||||
proxyArr := readArrayLine(v.String())
|
||||
//🚀 Proxy = select, 🌞 Line
|
||||
if len(proxyArr) > 1 {
|
||||
groupItems := proxyGroup{mode: proxyArr[0]}
|
||||
servers := make([]ProxyServer, len(proxyArr)-1)
|
||||
for i, p := range proxyArr[1:] {
|
||||
proxyName := strings.ToLower(p)
|
||||
servers[i] = config.proxyServer[proxyName]
|
||||
}
|
||||
groupItems.proxyServers = servers
|
||||
config.proxyGroup[groupName] = &groupItems
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//[Rule] Section
|
||||
func loadRules(config *ProxyConfig) {
|
||||
for _, key := range config.SurgeConfig.Section("Rule").KeyStrings() {
|
||||
if strings.HasPrefix(key, "//") {
|
||||
continue
|
||||
}
|
||||
items := readArrayLine(key)
|
||||
ruleName := strings.ToLower(items[0])
|
||||
switch ruleName {
|
||||
case "user-agent":
|
||||
config.ruleUserAgent = append(config.ruleUserAgent, &Rule{Match: items[1], Action: strings.ToLower(items[2])})
|
||||
case "domain-suffix":
|
||||
config.ruleSuffixDomains = append(config.ruleSuffixDomains, &Rule{Match: items[1], Action: strings.ToLower(items[2])})
|
||||
case "domain-prefix":
|
||||
config.rulePrefixDomains = append(config.rulePrefixDomains, &Rule{Match: items[1], Action: strings.ToLower(items[2])})
|
||||
case "domain-keyword":
|
||||
config.ruleKeywordDomains = append(config.ruleKeywordDomains, &Rule{Match: items[1], Action: strings.ToLower(items[2])})
|
||||
case "geoip":
|
||||
config.ruleGeoIP = append(config.ruleGeoIP, &Rule{Match: items[1], Action: strings.ToLower(items[2])})
|
||||
case "final":
|
||||
config.ruleFinal = &Rule{Match: "final", Action: strings.ToUpper(items[1])}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readArrayLine(source string) []string {
|
||||
@@ -157,120 +228,3 @@ func readArrayLine(source string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func RuleOfHost(host string) (result *DomainRule) {
|
||||
result = &DomainRule{S: "", T: RULE_DIRECT}
|
||||
hostParts := strings.Split(host, ":")
|
||||
domain := hostParts[0]
|
||||
|
||||
for _, rule := range ruleSuffixDomains {
|
||||
if strings.HasSuffix(domain, rule.S) {
|
||||
result = rule
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, rule := range rulePrefixDomains {
|
||||
if strings.HasPrefix(domain, rule.S) {
|
||||
result = rule
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, rule := range ruleKeywordDomains {
|
||||
if strings.Contains(domain, rule.S) {
|
||||
result = rule
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil || len(ips) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
country := GeoIPs(ips)
|
||||
log.Println("Found ip geo", country)
|
||||
if len(country) != 0 && ruleGeoIP.S == country {
|
||||
result = ruleGeoIP
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseServerConfig() {
|
||||
config := ssConfig
|
||||
hasPort := func(s string) bool {
|
||||
_, port, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return port != ""
|
||||
}
|
||||
|
||||
if len(config.ServerPassword) == 0 {
|
||||
method := config.Method
|
||||
if config.Auth {
|
||||
method += "-auth"
|
||||
}
|
||||
// only one encryption table
|
||||
cipher, err := ss.NewCipher(method, config.Password)
|
||||
if err != nil {
|
||||
log.Fatal("Failed generating ciphers:", err)
|
||||
}
|
||||
srvPort := strconv.Itoa(config.ServerPort)
|
||||
srvArr := config.GetServerArray()
|
||||
n := len(srvArr)
|
||||
ProxyServers.SrvCipher = make([]*ProxyServerCipher, n)
|
||||
|
||||
for i, s := range srvArr {
|
||||
if hasPort(s) {
|
||||
log.Println("ignore server_port option for server", s)
|
||||
ProxyServers.SrvCipher[i] = &ProxyServerCipher{s, cipher}
|
||||
} else {
|
||||
ProxyServers.SrvCipher[i] = &ProxyServerCipher{net.JoinHostPort(s, srvPort), cipher}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// multiple servers
|
||||
n := len(config.ServerPassword)
|
||||
ProxyServers.SrvCipher = make([]*ProxyServerCipher, n)
|
||||
|
||||
cipherCache := make(map[string]*ss.Cipher)
|
||||
i := 0
|
||||
for _, serverInfo := range config.ServerPassword {
|
||||
if len(serverInfo) < 2 || len(serverInfo) > 3 {
|
||||
log.Fatalf("server %v syntax error\n", serverInfo)
|
||||
}
|
||||
server := serverInfo[0]
|
||||
passwd := serverInfo[1]
|
||||
encmethod := ""
|
||||
if len(serverInfo) == 3 {
|
||||
encmethod = serverInfo[2]
|
||||
}
|
||||
if !hasPort(server) {
|
||||
log.Fatalf("no port for server %s\n", server)
|
||||
}
|
||||
// Using "|" as delimiter is safe here, since no encryption
|
||||
// method contains it in the name.
|
||||
cacheKey := encmethod + "|" + passwd
|
||||
cipher, ok := cipherCache[cacheKey]
|
||||
if !ok {
|
||||
var err error
|
||||
cipher, err = ss.NewCipher(encmethod, passwd)
|
||||
if err != nil {
|
||||
log.Fatal("Failed generating ciphers:", err)
|
||||
}
|
||||
cipherCache[cacheKey] = cipher
|
||||
}
|
||||
ProxyServers.SrvCipher[i] = &ProxyServerCipher{server, cipher}
|
||||
i++
|
||||
}
|
||||
}
|
||||
ProxyServers.FailCnt = make([]int, len(ProxyServers.SrvCipher))
|
||||
for _, se := range ProxyServers.SrvCipher {
|
||||
debug.Println("available remote server", se.Server)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
227
flora/flora.go
Normal file
227
flora/flora.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package flora
|
||||
|
||||
import (
|
||||
ss "github.com/shadowsocks/shadowsocks-go/shadowsocks"
|
||||
"net"
|
||||
"errors"
|
||||
"log"
|
||||
"fmt"
|
||||
"strings"
|
||||
"regexp"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
ServerTypeShadowSocks = "shadowsocks"
|
||||
ServerTypeCustom = "custom"
|
||||
ServerTypeHttp = "http"
|
||||
ServerTypeHttps = "https"
|
||||
ServerTypeDirect = "direct"
|
||||
ServerTypeReject = "direct"
|
||||
|
||||
LocalServerSocksV5 = "localSocksv5"
|
||||
LocalServerHttp = "localHttp"
|
||||
|
||||
socksVer5 = 5
|
||||
socksVer4 = 4
|
||||
httpProxy = 71
|
||||
socksCmdConnect = 1
|
||||
|
||||
typeIPv4 = 1 // type is ipv4 address
|
||||
typeDm = 3 // type is domain address
|
||||
typeIPv6 = 4 // type is ipv6 address
|
||||
)
|
||||
|
||||
type ProxyServer interface {
|
||||
//proxy type
|
||||
ProxyType() string
|
||||
//dial
|
||||
DialWithRawAddr(raw []byte, host string) (remote net.Conn, err error)
|
||||
//
|
||||
FailCount() int
|
||||
|
||||
AddFail()
|
||||
//
|
||||
ResetFailCount()
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Match string
|
||||
Action string
|
||||
}
|
||||
|
||||
var (
|
||||
errAddrType = errors.New("socks addr type not supported")
|
||||
errVer = errors.New("socks version not supported")
|
||||
errAuthExtraData = errors.New("socks authentication get extra data")
|
||||
errReqExtraData = errors.New("socks request get extra data")
|
||||
errCmd = errors.New("socks command not supported")
|
||||
errReject = errors.New("socks reject this request")
|
||||
errSupported = errors.New("proxy type not supported")
|
||||
)
|
||||
|
||||
var proxyConfig *ProxyConfig
|
||||
|
||||
|
||||
func Run(surgeCfg, geoipCfg string) {
|
||||
proxyConfig = LoadConfig(surgeCfg, geoipCfg)
|
||||
listenAddr := fmt.Sprintf("%s:%d", proxyConfig.LocalHost, proxyConfig.LocalSocksPort)
|
||||
initProxySettings(proxyConfig.systemBypass,listenAddr)
|
||||
ln, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Listen socket", listenAddr)
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Println("accept:", err)
|
||||
continue
|
||||
}
|
||||
go handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
isClose := false
|
||||
defer func() {
|
||||
if !isClose {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
var (
|
||||
host string
|
||||
hostType int
|
||||
err error
|
||||
rawData []byte
|
||||
)
|
||||
|
||||
buf := make([]byte, 1)
|
||||
io.ReadFull(conn, buf)
|
||||
|
||||
first := buf[0]
|
||||
switch first {
|
||||
case socksVer5:
|
||||
err = handshake(conn, first)
|
||||
host, hostType, err = socks5Connect(conn)
|
||||
case socksVer4:
|
||||
host, hostType, err = socks4Connect(conn, first)
|
||||
default:
|
||||
host, hostType, rawData, err = httpProxyConnect(conn, first)
|
||||
}
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
remote, err := matchRuleAndCreateConn(conn, host, hostType, rawData)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
//create remote connect
|
||||
defer func() {
|
||||
if !isClose {
|
||||
remote.Close()
|
||||
}
|
||||
}()
|
||||
go ss.PipeThenClose(conn, remote)
|
||||
ss.PipeThenClose(remote, conn)
|
||||
isClose = true
|
||||
}
|
||||
|
||||
func matchRuleAndCreateConn(conn net.Conn, addr string, hostType int, raw []byte) (net.Conn, error) {
|
||||
if nil == conn {
|
||||
return nil, errors.New("local connect is nil")
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(addr)
|
||||
var rule *Rule
|
||||
rule = matchBypass(host)
|
||||
if nil == rule {
|
||||
switch hostType {
|
||||
case typeIPv4, typeIPv6:
|
||||
rule = matchIpRule(host)
|
||||
case typeDm:
|
||||
rule = matchDomainRule(host)
|
||||
}
|
||||
}
|
||||
if nil == rule {
|
||||
if nil != proxyConfig.ruleFinal {
|
||||
rule = proxyConfig.ruleFinal
|
||||
} else {
|
||||
rule = &Rule{Match: "default", Action: ServerTypeDirect}
|
||||
}
|
||||
}
|
||||
return createRemoteConn(raw, rule, addr)
|
||||
}
|
||||
|
||||
func matchDomainRule(domain string) (*Rule) {
|
||||
for _, rule := range proxyConfig.ruleSuffixDomains {
|
||||
if strings.HasSuffix(domain, rule.Match) {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
for _, rule := range proxyConfig.rulePrefixDomains {
|
||||
if strings.HasPrefix(domain, rule.Match) {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
for _, rule := range proxyConfig.ruleKeywordDomains {
|
||||
if strings.Contains(domain, rule.Match) {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func matchIpRule(addr string) (*Rule) {
|
||||
ips := resolveRequestIPAddr(addr)
|
||||
if nil != ips {
|
||||
country := strings.ToLower(GeoIPs(ips))
|
||||
for _, rule := range proxyConfig.ruleGeoIP {
|
||||
if len(country) != 0 && strings.ToLower(rule.Match) == country {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func matchBypass(addr string) (*Rule) {
|
||||
ip := net.ParseIP(addr)
|
||||
for _, h := range proxyConfig.bypassDomains {
|
||||
var bypass bool = false
|
||||
var isIp = nil != ip
|
||||
switch h.(type) {
|
||||
case net.IP:
|
||||
if isIp {
|
||||
bypass = ip.Equal(h.(net.IP))
|
||||
}
|
||||
case *net.IPNet:
|
||||
if isIp {
|
||||
bypass = h.(*net.IPNet).Contains(ip)
|
||||
}
|
||||
case string:
|
||||
dm := h.(string)
|
||||
r, err := regexp.Compile(dm)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
bypass = r.MatchString(addr)
|
||||
}
|
||||
if bypass {
|
||||
return &Rule{Match: "bypass", Action: ServerTypeDirect}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createRemoteConn(raw []byte, rule *Rule, host string) (net.Conn, error) {
|
||||
server := proxyConfig.GetProxyServer(rule.Action)
|
||||
conn, err := server.DialWithRawAddr(raw, host)
|
||||
if nil != err {
|
||||
log.Printf("[%s]->[%s] 💊 [%s]", rule.Match, rule.Action, host)
|
||||
server.AddFail()
|
||||
} else {
|
||||
log.Printf("[%s]->[%s] ✅ [%s]", rule.Match, rule.Action, host)
|
||||
server.ResetFailCount()
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
@@ -9,9 +9,8 @@ import (
|
||||
|
||||
var geoDB *geoip2.Reader
|
||||
|
||||
func loadGeoIP() {
|
||||
file := "./geoip.mmdb"
|
||||
db, err := geoip2.Open(file)
|
||||
func loadGeoIP(geoFile string) {
|
||||
db, err := geoip2.Open(geoFile)
|
||||
// defer db.Close()
|
||||
if err != nil {
|
||||
log.Printf("Could not open GeoIP database\n")
|
||||
@@ -41,3 +40,20 @@ func GeoIP(ip net.IP) string {
|
||||
}
|
||||
return strings.ToLower(country.Country.IsoCode)
|
||||
}
|
||||
|
||||
func resolveRequestIPAddr(host string) []net.IP {
|
||||
var (
|
||||
ips []net.IP
|
||||
err error
|
||||
)
|
||||
ip := net.ParseIP(host)
|
||||
if nil == ip {
|
||||
ips, err = net.LookupIP(host)
|
||||
if err != nil || len(ips) == 0 {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
ips = []net.IP{ip}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
79
flora/http.go
Normal file
79
flora/http.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package flora
|
||||
|
||||
import (
|
||||
"net"
|
||||
"io"
|
||||
"net/http"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net/http/httputil"
|
||||
)
|
||||
|
||||
// local socks server connect
|
||||
func httpProxyConnect(conn net.Conn, first byte) (addr string, hostType int, raw []byte, err error) {
|
||||
var (
|
||||
HTTP_200 = []byte("HTTP/1.1 200 Connection Established\r\n\r\n")
|
||||
host string
|
||||
port string
|
||||
)
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
buf[0] = first
|
||||
io.ReadAtLeast(conn, buf[1:], 1)
|
||||
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(buf)))
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
host, port, err = net.SplitHostPort(req.Host)
|
||||
if nil != err {
|
||||
host = req.Host
|
||||
port = req.URL.Port()
|
||||
}
|
||||
scheme := req.URL.Scheme
|
||||
if "" == port {
|
||||
if scheme == "http" {
|
||||
port = "80"
|
||||
} else {
|
||||
port = "443"
|
||||
}
|
||||
}
|
||||
addr = net.JoinHostPort(host, port)
|
||||
method := req.Method
|
||||
hostType = getRequestType(addr)
|
||||
switch method {
|
||||
case http.MethodConnect:
|
||||
_, err = conn.Write(HTTP_200)
|
||||
default:
|
||||
removeHeaders(req)
|
||||
raw, err = httputil.DumpRequest(req, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getRequestType(addr string) int {
|
||||
host,_,_ := net.SplitHostPort(addr)
|
||||
ip := net.ParseIP(host)
|
||||
if nil != ip {
|
||||
return typeIPv4
|
||||
}
|
||||
return typeDm
|
||||
}
|
||||
|
||||
|
||||
func removeHeaders(req *http.Request) {
|
||||
req.RequestURI = ""
|
||||
req.Header.Del("Accept-Encoding")
|
||||
// curl can add that, see
|
||||
// https://jdebp.eu./FGA/web-proxy-connection-header.html
|
||||
req.Header.Del("Proxy-Connection")
|
||||
req.Header.Del("Proxy-Authenticate")
|
||||
req.Header.Del("Proxy-Authorization")
|
||||
//req.Header.Del("Referer")
|
||||
// Connection, Authenticate and Authorization are single hop Header:
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616.txt
|
||||
// 14.10 Connection
|
||||
// The Connection general-header field allows the sender to specify
|
||||
// options that are desired for that particular connection and MUST NOT
|
||||
// be communicated by proxies over further connections.
|
||||
req.Header.Del("Connection")
|
||||
}
|
||||
43
flora/network_setup.go
Normal file
43
flora/network_setup.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package flora
|
||||
|
||||
import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"os"
|
||||
"log"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SystemProxySettings interface {
|
||||
TurnOnGlobProxy()
|
||||
TurnOffGlobProxy()
|
||||
}
|
||||
|
||||
var sigs = make(chan os.Signal, 1)
|
||||
|
||||
func resetProxySettings(proxySettings SystemProxySettings) {
|
||||
for {
|
||||
select {
|
||||
case <-sigs:
|
||||
log.Print("Flora-kit is shutdown now ...")
|
||||
proxySettings.TurnOffGlobProxy()
|
||||
time.Sleep(time.Duration(2000))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initProxySettings(bypass []string, addr string) {
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
var proxySettings SystemProxySettings
|
||||
if runtime.GOOS == "windows" {
|
||||
w := &windows{addr}
|
||||
proxySettings = w
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
d := &darwin{bypass,addr}
|
||||
proxySettings = d
|
||||
}
|
||||
proxySettings.TurnOnGlobProxy()
|
||||
go resetProxySettings(proxySettings)
|
||||
}
|
||||
@@ -2,22 +2,22 @@ package flora
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"net"
|
||||
)
|
||||
|
||||
type darwin struct {
|
||||
bypassDomains []string
|
||||
address string
|
||||
}
|
||||
|
||||
type execNetworkFunc func(name string)
|
||||
|
||||
var allow_services = "Wi-Fi|Thunderbolt Bridge|Thunderbolt Ethernet"
|
||||
|
||||
func ResetAllProxys() {
|
||||
if runtime.GOOS != "darwin" {
|
||||
log.Println("WARN: Your not in macOS, Networksetup skiped. Please change Network proxy setting by manually.")
|
||||
}
|
||||
|
||||
func (d *darwin) TurnOffGlobProxy() {
|
||||
execNetworks(func(name string) {
|
||||
runNetworksetup("-setftpproxystate", name, "off")
|
||||
runNetworksetup("-setwebproxystate", name, "off")
|
||||
@@ -29,24 +29,21 @@ func ResetAllProxys() {
|
||||
})
|
||||
}
|
||||
|
||||
func SetSocksFirewallProxy() {
|
||||
execNetworks(func(name string) {
|
||||
runNetworksetup("-setsocksfirewallproxy", name, "127.0.0.1", fmt.Sprintf("%d", SOCKS_PORT))
|
||||
})
|
||||
}
|
||||
func (d *darwin) TurnOnGlobProxy() {
|
||||
host, port, _ := net.SplitHostPort(d.address)
|
||||
|
||||
execNetworks(func(name string) {
|
||||
runNetworksetup("-setsocksfirewallproxy", name, host, port)
|
||||
})
|
||||
|
||||
func SetProxyBypassDomains(domains []string) {
|
||||
execNetworks(func(name string) {
|
||||
args := []string{"-setproxybypassdomains", name}
|
||||
args = append(args, domains...)
|
||||
args = append(args, d.bypassDomains...)
|
||||
runNetworksetup(args...)
|
||||
})
|
||||
}
|
||||
|
||||
func runNetworksetup(args ...string) string {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// log.Println("networksetup", args)
|
||||
cmd := exec.Command("networksetup", args...)
|
||||
47
flora/network_setup_win.go
Normal file
47
flora/network_setup_win.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package flora
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"log"
|
||||
)
|
||||
|
||||
type windows struct {
|
||||
address string
|
||||
}
|
||||
|
||||
const (
|
||||
cmdRegistry = `reg`
|
||||
cmdRegistryAdd = `add`
|
||||
internetSettingsKey = `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings`
|
||||
keyProxyEnable = `ProxyEnable`
|
||||
keyProxyServer = `ProxyServer`
|
||||
dataTypeDWord = `REG_DWORD`
|
||||
dataTypeRegSZ = `REG_SZ`
|
||||
)
|
||||
|
||||
func (w *windows) TurnOnGlobProxy() {
|
||||
c := exec.Command(cmdRegistry, cmdRegistryAdd, internetSettingsKey, `/v`, keyProxyEnable, `/t`, dataTypeDWord, `/d`, `1`, `/f`)
|
||||
var err error
|
||||
if _, err = c.CombinedOutput(); err != nil {
|
||||
log.Printf("enable windows proxy has error %s", err)
|
||||
}
|
||||
c = exec.Command(cmdRegistry, cmdRegistryAdd, internetSettingsKey, `/v`, keyProxyServer, `/t`, dataTypeRegSZ, `/d`, w.address, `/f`)
|
||||
if _, err = c.CombinedOutput(); err != nil {
|
||||
log.Printf("Windows global proxy settings has error %s , Try to set it manually ", err)
|
||||
}
|
||||
if nil == err {
|
||||
log.Print("Windows global proxy settings are successful ,Please use after 2 minutes ...")
|
||||
}
|
||||
}
|
||||
|
||||
// TurnOffSystemProxy
|
||||
func (w *windows) TurnOffGlobProxy() {
|
||||
var err error
|
||||
c := exec.Command(cmdRegistry, cmdRegistryAdd, internetSettingsKey, `/v`, keyProxyEnable, `/t`, dataTypeDWord, `/d`, `0`, `/f`)
|
||||
if _, err = c.CombinedOutput(); err != nil {
|
||||
log.Printf("disable windows proxy has error %s", err)
|
||||
}
|
||||
if nil == err{
|
||||
log.Print("disable windows proxy settings are successful ...")
|
||||
}
|
||||
}
|
||||
38
flora/proxy_direct.go
Normal file
38
flora/proxy_direct.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package flora
|
||||
|
||||
import "net"
|
||||
|
||||
type DirectServer struct {
|
||||
proxyType string
|
||||
}
|
||||
|
||||
func NewDirect() (*DirectServer) {
|
||||
return &DirectServer{proxyType: ServerTypeDirect}
|
||||
}
|
||||
|
||||
func (s *DirectServer) FailCount() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *DirectServer) ResetFailCount() {
|
||||
|
||||
}
|
||||
|
||||
func (s *DirectServer) AddFail() {
|
||||
|
||||
}
|
||||
|
||||
func (s *DirectServer) ProxyType() string {
|
||||
return s.proxyType
|
||||
}
|
||||
|
||||
func (s *DirectServer) DialWithRawAddr(raw []byte, host string) (remote net.Conn, err error) {
|
||||
conn, err := net.Dial("tcp", host)
|
||||
if nil != err{
|
||||
return nil,err
|
||||
}
|
||||
if nil != raw && len(raw) > 0 {
|
||||
conn.Write(raw)
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
31
flora/proxy_reject.go
Normal file
31
flora/proxy_reject.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package flora
|
||||
|
||||
import "net"
|
||||
|
||||
type Reject struct {
|
||||
proxyType string
|
||||
}
|
||||
|
||||
func NewReject() (*Reject) {
|
||||
return &Reject{proxyType: ServerTypeReject}
|
||||
}
|
||||
|
||||
func (s *Reject) FailCount() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *Reject) ResetFailCount() {
|
||||
|
||||
}
|
||||
|
||||
func (s *Reject) AddFail() {
|
||||
|
||||
}
|
||||
|
||||
func (s *Reject) ProxyType() string {
|
||||
return s.proxyType
|
||||
}
|
||||
|
||||
func (s *Reject) DialWithRawAddr(raw []byte, host string) (remote net.Conn, err error) {
|
||||
return nil, errReject
|
||||
}
|
||||
51
flora/proxy_shadowsocks.go
Normal file
51
flora/proxy_shadowsocks.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package flora
|
||||
|
||||
import (
|
||||
"net"
|
||||
ss "github.com/shadowsocks/shadowsocks-go/shadowsocks"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ShadowSocksServer struct {
|
||||
proxyType string
|
||||
server string
|
||||
cipher *ss.Cipher
|
||||
failCount int
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewShadowSocks(server string, cipher *ss.Cipher) (*ShadowSocksServer) {
|
||||
return &ShadowSocksServer{
|
||||
proxyType: ServerTypeShadowSocks,
|
||||
server: server,
|
||||
cipher: cipher,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ShadowSocksServer) ResetFailCount() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.failCount = 0
|
||||
}
|
||||
|
||||
func (s *ShadowSocksServer) AddFail() {
|
||||
s.failCount ++
|
||||
}
|
||||
|
||||
func (s *ShadowSocksServer) FailCount() int {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
return s.failCount
|
||||
}
|
||||
|
||||
func (s *ShadowSocksServer) ProxyType() string {
|
||||
return s.proxyType
|
||||
}
|
||||
|
||||
func (s *ShadowSocksServer) DialWithRawAddr(raw []byte, host string) (net.Conn, error) {
|
||||
if nil != raw && len(raw) > 0 {
|
||||
return ss.DialWithRawAddr(raw, s.server, s.cipher.Copy())
|
||||
} else {
|
||||
return ss.Dial(host, s.server, s.cipher.Copy())
|
||||
}
|
||||
}
|
||||
78
flora/socks4.go
Normal file
78
flora/socks4.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package flora
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
socks4 protocol
|
||||
|
||||
request
|
||||
byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... |
|
||||
|0x04|cmd| port | ip | user\0 |
|
||||
|
||||
reply
|
||||
byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7|
|
||||
|0x00|status| | |
|
||||
|
||||
|
||||
socks4a protocol
|
||||
|
||||
request
|
||||
byte | 0 | 1 | 2 | 3 |4 | 5 | 6 | 7 | 8 | ... |... |
|
||||
|0x04|cmd| port | 0.0.0.x | user\0 |domain\0|
|
||||
|
||||
reply
|
||||
byte | 0 | 1 | 2 | 3 | 4 | 5 | 6| 7 |
|
||||
|0x00|staus| port | ip |
|
||||
|
||||
*/
|
||||
|
||||
// local socks server connect
|
||||
func socks4Connect(conn net.Conn,first byte ) (addr string, hostType int, err error) {
|
||||
const (
|
||||
idVer = 0
|
||||
idStatus = 1
|
||||
idPort = 2 // address type index
|
||||
idPortLen = 2
|
||||
idIP = 4 // ip addres start index
|
||||
idIPLen = 4 // domain address length index
|
||||
|
||||
idVariable = 8
|
||||
id4aFixLen = 8
|
||||
cmdConnect = 1
|
||||
)
|
||||
// refer to getRequest in flora.go for why set buffer size to 263
|
||||
buf := make([]byte, 128)
|
||||
buf[idVer] = first
|
||||
var n int
|
||||
// read till we get possible domain length field
|
||||
if n, err = io.ReadAtLeast(conn, buf[1:], id4aFixLen); err != nil {
|
||||
return
|
||||
}
|
||||
n ++
|
||||
// command only support connect
|
||||
if buf[idStatus] != cmdConnect {
|
||||
return
|
||||
}
|
||||
// get port
|
||||
port := binary.BigEndian.Uint16(buf[idPort:idPort+idPortLen])
|
||||
|
||||
// get ip
|
||||
ip := net.IP(buf[idIP:idIP+idIPLen])
|
||||
hostType = typeIPv4
|
||||
var host = ip.String()
|
||||
|
||||
//socks4a
|
||||
if ip[0] == 0x00 && ip[1] == 0x00 && ip[2] == 0x00 && ip[3] != 0x00 && n+1 >= id4aFixLen {
|
||||
dm := buf[idVariable:n]
|
||||
host = string(dm)
|
||||
hostType = typeDm
|
||||
}
|
||||
addr = net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
_, err = conn.Write([]byte{0x00, 0x5a, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00})
|
||||
return
|
||||
}
|
||||
158
flora/socks5.go
Normal file
158
flora/socks5.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package flora
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
ss "github.com/shadowsocks/shadowsocks-go/shadowsocks"
|
||||
"encoding/binary"
|
||||
"log"
|
||||
)
|
||||
|
||||
/*
|
||||
socks5 protocol
|
||||
|
||||
initial
|
||||
|
||||
byte | 0 | 1 | 2 | ...... | n |
|
||||
|0x05|num auth| auth methods |
|
||||
|
||||
|
||||
reply
|
||||
|
||||
byte | 0 | 1 |
|
||||
|0x05| auth|
|
||||
|
||||
|
||||
username/password auth request
|
||||
|
||||
byte | 0 | 1 | | 1 byte | |
|
||||
|0x01|username_len| username | password_len | password |
|
||||
|
||||
username/password auth reponse
|
||||
|
||||
byte | 0 | 1 |
|
||||
|0x01|status|
|
||||
|
||||
request
|
||||
|
||||
byte | 0 | 1 | 2 | 3 | 4 | .. | n-2 | n-1| n |
|
||||
|0x05|cmd|0x00|addrtype| addr | port |
|
||||
|
||||
response
|
||||
byte |0 | 1 | 2 | 3 | 4 | .. | n-2 | n-1 | n |
|
||||
|0x05|status|0x00|addrtype| addr | port |
|
||||
|
||||
*/
|
||||
|
||||
//local socks server auth
|
||||
func handshake(conn net.Conn,first byte ) (err error) {
|
||||
const (
|
||||
idVer = 0
|
||||
idNmethod = 1
|
||||
)
|
||||
// version identification and method selection message in theory can have
|
||||
// at most 256 methods, plus version and nmethod field in total 258 bytes
|
||||
// the current rfc defines only 3 authentication methods (plus 2 reserved),
|
||||
// so it won't be such long in practice
|
||||
|
||||
buf := make([]byte, 258)
|
||||
buf[idVer] = first
|
||||
var n int
|
||||
ss.SetReadTimeout(conn)
|
||||
// make sure we get the nmethod field
|
||||
if n, err = io.ReadAtLeast(conn, buf[1:], idNmethod+1); err != nil {
|
||||
return
|
||||
}
|
||||
n ++
|
||||
//if buf[idVer] != socksVer5 {
|
||||
// return errVer
|
||||
//}
|
||||
nmethod := int(buf[idNmethod])
|
||||
msgLen := nmethod + 2
|
||||
if n == msgLen { // handshake done, common case
|
||||
// do nothing, jump directly to send confirmation
|
||||
} else if n < msgLen { // has more methods to read, rare case
|
||||
if _, err = io.ReadFull(conn, buf[n:msgLen]); err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
} else { // error, should not get extra data
|
||||
return errAuthExtraData
|
||||
}
|
||||
// send confirmation: version 5, no authentication required
|
||||
_, err = conn.Write([]byte{socksVer5, 0})
|
||||
return
|
||||
}
|
||||
|
||||
// local socks server connect
|
||||
func socks5Connect(conn net.Conn) (host string, hostType int, err error) {
|
||||
const (
|
||||
idVer = 0
|
||||
idCmd = 1
|
||||
idType = 3 // address type index
|
||||
idIP0 = 4 // ip addres start index
|
||||
idDmLen = 4 // domain address length index
|
||||
idDm0 = 5 // domain address start index
|
||||
|
||||
lenIPv4 = 3 + 1 + net.IPv4len + 2 // 3(ver+cmd+rsv) + 1addrType + ipv4 + 2port
|
||||
lenIPv6 = 3 + 1 + net.IPv6len + 2 // 3(ver+cmd+rsv) + 1addrType + ipv6 + 2port
|
||||
lenDmBase = 3 + 1 + 1 + 2 // 3 + 1addrType + 1addrLen + 2port, plus addrLen
|
||||
)
|
||||
// refer to getRequest in flora.go for why set buffer size to 263
|
||||
buf := make([]byte, 263)
|
||||
var n int
|
||||
ss.SetReadTimeout(conn)
|
||||
// read till we get possible domain length field
|
||||
if n, err = io.ReadAtLeast(conn, buf, idDmLen+1); err != nil {
|
||||
return
|
||||
}
|
||||
// check version and cmd
|
||||
//if buf[idVer] != socksVer5 {
|
||||
// err = errVer
|
||||
// return
|
||||
//}
|
||||
if buf[idCmd] != socksCmdConnect {
|
||||
err = errCmd
|
||||
return
|
||||
}
|
||||
|
||||
reqLen := -1
|
||||
hostType = int(buf[idType])
|
||||
switch hostType {
|
||||
case typeIPv4:
|
||||
reqLen = lenIPv4
|
||||
case typeIPv6:
|
||||
reqLen = lenIPv6
|
||||
case typeDm:
|
||||
reqLen = int(buf[idDmLen]) + lenDmBase
|
||||
default:
|
||||
err = errAddrType
|
||||
return
|
||||
}
|
||||
|
||||
if n == reqLen {
|
||||
// common case, do nothing
|
||||
} else if n < reqLen { // rare case
|
||||
if _, err = io.ReadFull(conn, buf[n:reqLen]); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = errReqExtraData
|
||||
return
|
||||
}
|
||||
|
||||
//raw := buf[idType:reqLen]
|
||||
switch hostType {
|
||||
case typeIPv4:
|
||||
host = net.IP(buf[idIP0: idIP0+net.IPv4len]).String()
|
||||
case typeIPv6:
|
||||
host = net.IP(buf[idIP0: idIP0+net.IPv6len]).String()
|
||||
case typeDm:
|
||||
host = string(buf[idDm0: idDm0+buf[idDmLen]])
|
||||
}
|
||||
port := binary.BigEndian.Uint16(buf[reqLen-2: reqLen])
|
||||
host = net.JoinHostPort(host, strconv.Itoa(int(port)))
|
||||
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43})
|
||||
return
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"github.com/huacnlee/flora-kit/flora"
|
||||
"flora-kit/flora"
|
||||
"testing"
|
||||
"os"
|
||||
)
|
||||
|
||||
func TestGeoIP(t *testing.T) {
|
||||
p, _ := os.Getwd()
|
||||
flora.LoadConfig("", p+"/geoip.mmdb")
|
||||
|
||||
if flora.GeoIPString("121.0.29.91") != "cn" {
|
||||
t.Errorf("121.0.29.91 should be cn")
|
||||
}
|
||||
@@ -17,4 +21,8 @@ func TestGeoIP(t *testing.T) {
|
||||
if flora.GeoIPString("218.176.242.11") != "jp" {
|
||||
t.Errorf("218.176.242.11 should be jp")
|
||||
}
|
||||
|
||||
if flora.GeoIPString("8.8.8.8") != "us" {
|
||||
t.Errorf("218.176.242.11 should be jp")
|
||||
}
|
||||
}
|
||||
|
||||
28
goreleaser.yml
Normal file
28
goreleaser.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
# goreleaser.yml
|
||||
# Build customization
|
||||
build:
|
||||
main: main.go
|
||||
binary: flora-kit
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
ldflags: -s -w
|
||||
release:
|
||||
github:
|
||||
owner: huacnlee
|
||||
name: flora-kit
|
||||
# Archive customization
|
||||
archive:
|
||||
format: tar.gz
|
||||
replacements:
|
||||
amd64: 64-bit
|
||||
darwin: macOS
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
- CHANGELOG.md
|
||||
- flora.default.conf
|
||||
- geoip.mmdb
|
||||
279
main.go
279
main.go
@@ -1,281 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/huacnlee/flora-kit/flora"
|
||||
ss "github.com/shadowsocks/shadowsocks-go/shadowsocks"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
"flora-kit/flora"
|
||||
"flag"
|
||||
)
|
||||
|
||||
var debug ss.DebugLog
|
||||
|
||||
var (
|
||||
errAddrType = errors.New("socks addr type not supported")
|
||||
errVer = errors.New("socks version not supported")
|
||||
errMethod = errors.New("socks only support 1 method now")
|
||||
errAuthExtraData = errors.New("socks authentication get extra data")
|
||||
errReqExtraData = errors.New("socks request get extra data")
|
||||
errCmd = errors.New("socks command not supported")
|
||||
)
|
||||
|
||||
const (
|
||||
socksVer5 = 5
|
||||
socksCmdConnect = 1
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
}
|
||||
|
||||
func handShake(conn net.Conn) (err error) {
|
||||
const (
|
||||
idVer = 0
|
||||
idNmethod = 1
|
||||
)
|
||||
// version identification and method selection message in theory can have
|
||||
// at most 256 methods, plus version and nmethod field in total 258 bytes
|
||||
// the current rfc defines only 3 authentication methods (plus 2 reserved),
|
||||
// so it won't be such long in practice
|
||||
|
||||
buf := make([]byte, 258)
|
||||
|
||||
var n int
|
||||
ss.SetReadTimeout(conn)
|
||||
// make sure we get the nmethod field
|
||||
if n, err = io.ReadAtLeast(conn, buf, idNmethod+1); err != nil {
|
||||
return
|
||||
}
|
||||
if buf[idVer] != socksVer5 {
|
||||
return errVer
|
||||
}
|
||||
nmethod := int(buf[idNmethod])
|
||||
msgLen := nmethod + 2
|
||||
if n == msgLen { // handshake done, common case
|
||||
// do nothing, jump directly to send confirmation
|
||||
} else if n < msgLen { // has more methods to read, rare case
|
||||
if _, err = io.ReadFull(conn, buf[n:msgLen]); err != nil {
|
||||
return
|
||||
}
|
||||
} else { // error, should not get extra data
|
||||
return errAuthExtraData
|
||||
}
|
||||
// send confirmation: version 5, no authentication required
|
||||
_, err = conn.Write([]byte{socksVer5, 0})
|
||||
return
|
||||
}
|
||||
|
||||
func getRequest(conn net.Conn) (rawaddr []byte, host string, err error) {
|
||||
const (
|
||||
idVer = 0
|
||||
idCmd = 1
|
||||
idType = 3 // address type index
|
||||
idIP0 = 4 // ip addres start index
|
||||
idDmLen = 4 // domain address length index
|
||||
idDm0 = 5 // domain address start index
|
||||
|
||||
typeIPv4 = 1 // type is ipv4 address
|
||||
typeDm = 3 // type is domain address
|
||||
typeIPv6 = 4 // type is ipv6 address
|
||||
|
||||
lenIPv4 = 3 + 1 + net.IPv4len + 2 // 3(ver+cmd+rsv) + 1addrType + ipv4 + 2port
|
||||
lenIPv6 = 3 + 1 + net.IPv6len + 2 // 3(ver+cmd+rsv) + 1addrType + ipv6 + 2port
|
||||
lenDmBase = 3 + 1 + 1 + 2 // 3 + 1addrType + 1addrLen + 2port, plus addrLen
|
||||
)
|
||||
// refer to getRequest in server.go for why set buffer size to 263
|
||||
buf := make([]byte, 263)
|
||||
var n int
|
||||
ss.SetReadTimeout(conn)
|
||||
// read till we get possible domain length field
|
||||
if n, err = io.ReadAtLeast(conn, buf, idDmLen+1); err != nil {
|
||||
return
|
||||
}
|
||||
// check version and cmd
|
||||
if buf[idVer] != socksVer5 {
|
||||
err = errVer
|
||||
return
|
||||
}
|
||||
if buf[idCmd] != socksCmdConnect {
|
||||
err = errCmd
|
||||
return
|
||||
}
|
||||
|
||||
reqLen := -1
|
||||
switch buf[idType] {
|
||||
case typeIPv4:
|
||||
reqLen = lenIPv4
|
||||
case typeIPv6:
|
||||
reqLen = lenIPv6
|
||||
case typeDm:
|
||||
reqLen = int(buf[idDmLen]) + lenDmBase
|
||||
default:
|
||||
err = errAddrType
|
||||
return
|
||||
}
|
||||
|
||||
if n == reqLen {
|
||||
// common case, do nothing
|
||||
} else if n < reqLen { // rare case
|
||||
if _, err = io.ReadFull(conn, buf[n:reqLen]); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = errReqExtraData
|
||||
return
|
||||
}
|
||||
|
||||
rawaddr = buf[idType:reqLen]
|
||||
|
||||
switch buf[idType] {
|
||||
case typeIPv4:
|
||||
host = net.IP(buf[idIP0 : idIP0+net.IPv4len]).String()
|
||||
case typeIPv6:
|
||||
host = net.IP(buf[idIP0 : idIP0+net.IPv6len]).String()
|
||||
case typeDm:
|
||||
host = string(buf[idDm0 : idDm0+buf[idDmLen]])
|
||||
}
|
||||
port := binary.BigEndian.Uint16(buf[reqLen-2 : reqLen])
|
||||
host = net.JoinHostPort(host, strconv.Itoa(int(port)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func connectToServer(serverId int, rawaddr []byte, addr string) (remote *ss.Conn, err error) {
|
||||
se := flora.ProxyServers.SrvCipher[serverId]
|
||||
remote, err = ss.DialWithRawAddr(rawaddr, se.Server, se.Cipher.Copy())
|
||||
if err != nil {
|
||||
log.Println("error connecting to shadowsocks server:", err)
|
||||
const maxFailCnt = 30
|
||||
if flora.ProxyServers.FailCnt[serverId] < maxFailCnt {
|
||||
flora.ProxyServers.FailCnt[serverId]++
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
debug.Printf("connected to %s via %s\n", addr, se.Server)
|
||||
flora.ProxyServers.FailCnt[serverId] = 0
|
||||
return
|
||||
}
|
||||
|
||||
// Connection to the server in the order specified in the config. On
|
||||
// connection failure, try the next server. A failed server will be tried with
|
||||
// some probability according to its fail count, so we can discover recovered
|
||||
// servers.
|
||||
func createServerConn(rawaddr []byte, addr string) (remote *ss.Conn, err error) {
|
||||
const baseFailCnt = 20
|
||||
n := len(flora.ProxyServers.SrvCipher)
|
||||
skipped := make([]int, 0)
|
||||
for i := 0; i < n; i++ {
|
||||
// skip failed server, but try it with some probability
|
||||
if flora.ProxyServers.FailCnt[i] > 0 && rand.Intn(flora.ProxyServers.FailCnt[i]+baseFailCnt) != 0 {
|
||||
skipped = append(skipped, i)
|
||||
continue
|
||||
}
|
||||
remote, err = connectToServer(i, rawaddr, addr)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
// last resort, try skipped servers, not likely to succeed
|
||||
for _, i := range skipped {
|
||||
remote, err = connectToServer(i, rawaddr, addr)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接请求入口
|
||||
func handleConnection(conn net.Conn) {
|
||||
closed := false
|
||||
defer func() {
|
||||
if !closed {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var err error = nil
|
||||
if err = handShake(conn); err != nil {
|
||||
log.Println("socks handshake:", err)
|
||||
return
|
||||
}
|
||||
rawaddr, host, err := getRequest(conn)
|
||||
if err != nil {
|
||||
log.Println("error getting request:", err)
|
||||
return
|
||||
}
|
||||
// Sending connection established message immediately to client.
|
||||
// This some round trip time for creating socks connection with the client.
|
||||
// But if connection failed, the client will get connection reset error.
|
||||
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43})
|
||||
if err != nil {
|
||||
debug.Println("send connection confirmation:", err)
|
||||
return
|
||||
}
|
||||
|
||||
rule := flora.RuleOfHost(host)
|
||||
if rule.T == flora.RULE_REJECT {
|
||||
log.Println("REJECT", host)
|
||||
return
|
||||
}
|
||||
|
||||
var remote net.Conn
|
||||
if rule.T == flora.RULE_DIRECT {
|
||||
log.Println("DIRECT", host)
|
||||
remote, err = net.Dial("tcp", host)
|
||||
if err != nil {
|
||||
log.Println("Failed connect to all remote server via Direct")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Println("PROXY ", host)
|
||||
remote, err = createServerConn(rawaddr, host)
|
||||
if err != nil {
|
||||
if len(flora.ProxyServers.SrvCipher) > 1 {
|
||||
log.Println("Failed connect to all avaiable shadowsocks server")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if !closed {
|
||||
remote.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
go ss.PipeThenClose(conn, remote)
|
||||
ss.PipeThenClose(remote, conn)
|
||||
closed = true
|
||||
}
|
||||
|
||||
func run(listenAddr string) {
|
||||
flora.ResetAllProxys()
|
||||
|
||||
ln, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Listen socks", listenAddr)
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Println("accept:", err)
|
||||
continue
|
||||
}
|
||||
go handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
ss.SetDebug(true)
|
||||
log.Println("Floar", flora.VERSION)
|
||||
run("0.0.0.0:" + fmt.Sprintf("%d", flora.SOCKS_PORT))
|
||||
var configFile, geoipdb string
|
||||
flag.StringVar(&configFile, "s", "flora.default.conf", "specify surge config file")
|
||||
flag.StringVar(&geoipdb, "d", "geoip.mmdb", "specify geoip db file")
|
||||
flora.Run(configFile, geoipdb)
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user