diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..f9fa231 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e4fd83 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Yrr0r + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/client.go b/client.go new file mode 100755 index 0000000..7fe24d6 --- /dev/null +++ b/client.go @@ -0,0 +1,54 @@ +//CLIENT + +package main + +import ( + "fmt" + "log" + "net" + + "./cmd" + "./core" + "./local" +) + +var version = "master" + +func main() { + fmt.Print("Ich*Liebe*Dich~ \n", "CLIENT\n") + log.SetFlags(log.Lshortfile) + + // 默认配置 + config := &cmd.Config{ + ListenAddr: ":7448", + } + config.ReadConfig() + + // 解析配置 + password, err := core.ParsePassword(config.Password) + if err != nil { + log.Fatalln(err) + } + listenAddr, err := net.ResolveTCPAddr("tcp", config.ListenAddr) + if err != nil { + log.Fatalln(err) + } + remoteAddr, err := net.ResolveTCPAddr("tcp", config.RemoteAddr) + if err != nil { + log.Fatalln(err) + } + + // 启动 local 端并监听 + lsLocal := local.New(password, listenAddr, remoteAddr) + log.Fatalln(lsLocal.Listen(func(listenAddr net.Addr) { + log.Println("使用配置:", fmt.Sprintf(` +本地监听地址 listen: +%s +远程服务地址 remote: +%s +密码 password: +%s + `, listenAddr, remoteAddr, password)) + log.Printf("lightsocks-local:%s 启动成功 监听在 %s\n", version, listenAddr.String()) + })) +} diff --git a/cmd/.DS_Store b/cmd/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/cmd/.DS_Store differ diff --git a/cmd/config.go b/cmd/config.go new file mode 100755 index 0000000..59e3b88 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "path" + + "flag" + + "../core" +) + +var ( + // 配置文件路径 + configPath string +) + +var listen string +var passwd string +var remote string + +//Config temporary structure +type Config struct { + ListenAddr string `json:"listen"` + RemoteAddr string `json:"remote"` + Password string `json:"password"` +} + +func init() { + + flag.StringVar(&configPath, "conf", "NO", "配置文件路径") + + flag.StringVar(&listen, "port", "EMPTY", "服务器端口") + flag.StringVar(&passwd, "pass", "EMPTY", "密码") + flag.StringVar(&remote, "remote", "EMPTY", "remote") + + var confgen = flag.String("gconf", "NO", "用此命令生成配置文件然后手动填入参数。") + + flag.Usage = func() { + flag.PrintDefaults() + } + + flag.Parse() + + if *confgen != "NO" { + configPath = *confgen + content := Config{ + ListenAddr: ":7448", + // 密码随机生成 + Password: core.RandPassword().String(), + } + configJson, _ := json.MarshalIndent(content, "", " ") + err := ioutil.WriteFile(configPath, configJson, 0644) + if err != nil { + fmt.Errorf("保存配置到文件出错: ", configPath, err) + } + log.Printf("保存配置到文件成功", configPath, "\n") + os.Exit(0) + } + if configPath == "NO" { + file, err := os.Getwd() + configPath = path.Join(file, "conf.json") + fmt.Println(configPath) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + +} + +func (config *Config) ReadConfig() { + // 如果配置文件存在,就读取配置文件中的配置 assign 到 config + if _, err := os.Stat(configPath); !os.IsNotExist(err) { + log.Printf("读取配置:", configPath, "\n") + file, err := os.Open(configPath) + if err != nil { + log.Fatalf("打开配置文件 %s 出错:%s", configPath, err) + } + defer file.Close() + + err = json.NewDecoder(file).Decode(config) + if err != nil { + log.Fatalf("格式不合法的 JSON 配置文件:\n%s", file) + } + } + if listen != "EMPTY" { + config.ListenAddr = ":" + listen + fmt.Println("端口:命令行参数先于配置文件") + } + if passwd != "EMPTY" { + config.Password = passwd + fmt.Println("密码:命令行参数先于配置文件") + } + if remote != "EMPTY" { + config.RemoteAddr = remote + fmt.Println("Remote:命令行参数先于配置文件") + } + +} diff --git a/core/cipher.go b/core/cipher.go new file mode 100755 index 0000000..3c68d05 --- /dev/null +++ b/core/cipher.go @@ -0,0 +1,35 @@ +package core + +type Cipher struct { + // 编码用的密码 + encodePassword *Password + // 解码用的密码 + decodePassword *Password +} + +// 加密原数据 +func (cipher *Cipher) encode(bs []byte) { + for i, v := range bs { + bs[i] = cipher.encodePassword[v] + } +} + +// 解码加密后的数据到原数据 +func (cipher *Cipher) decode(bs []byte) { + for i, v := range bs { + bs[i] = cipher.decodePassword[v] + } +} + +// 新建一个编码解码器 +func NewCipher(encodePassword *Password) *Cipher { + decodePassword := &Password{} + for i, v := range encodePassword { + encodePassword[i] = v + decodePassword[v] = byte(i) + } + return &Cipher{ + encodePassword: encodePassword, + decodePassword: decodePassword, + } +} diff --git a/core/cipher_test.go b/core/cipher_test.go new file mode 100755 index 0000000..aad73e3 --- /dev/null +++ b/core/cipher_test.go @@ -0,0 +1,54 @@ +package core + +import ( + "crypto/rand" + "reflect" + "testing" +) + +const ( + MB = 1024 * 1024 +) + +// 测试 Cipher 加密解密 +func TestCipher(t *testing.T) { + password := RandPassword() + t.Log(password) + cipher := NewCipher(password) + // 原数据 + org := make([]byte, PasswordLength) + for i := 0; i < PasswordLength; i++ { + org[i] = byte(i) + } + // 复制一份原数据到 tmp + tmp := make([]byte, PasswordLength) + copy(tmp, org) + t.Log(tmp) + // 加密 tmp + cipher.encode(tmp) + t.Log(tmp) + // 解密 tmp + cipher.decode(tmp) + t.Log(tmp) + if !reflect.DeepEqual(org, tmp) { + t.Error("解码编码数据后无法还原数据,数据不对应") + } +} + +func BenchmarkEncode(b *testing.B) { + password := RandPassword() + cipher := NewCipher(password) + bs := make([]byte, MB) + b.ResetTimer() + rand.Read(bs) + cipher.encode(bs) +} + +func BenchmarkDecode(b *testing.B) { + password := RandPassword() + cipher := NewCipher(password) + bs := make([]byte, MB) + b.ResetTimer() + rand.Read(bs) + cipher.decode(bs) +} diff --git a/core/password.go b/core/password.go new file mode 100755 index 0000000..fc1282b --- /dev/null +++ b/core/password.go @@ -0,0 +1,53 @@ +package core + +import ( + "encoding/base64" + "errors" + "math/rand" + "strings" + "time" +) + +const PasswordLength = 256 + +var ErrInvalidPassword = errors.New("不合法的密码") + +type Password [PasswordLength]byte + +func init() { + // 更新随机种子,防止生成一样的随机密码 + rand.Seed(time.Now().Unix()) +} + +// 采用base64编码把密码转换为字符串 +func (password *Password) String() string { + return base64.StdEncoding.EncodeToString(password[:]) +} + +// 解析采用base64编码的字符串获取密码 +func ParsePassword(passwordString string) (*Password, error) { + bs, err := base64.StdEncoding.DecodeString(strings.TrimSpace(passwordString)) + if err != nil || len(bs) != PasswordLength { + return nil, ErrInvalidPassword + } + password := Password{} + copy(password[:], bs) + bs = nil + return &password, nil +} + +// 产生 256个byte随机组合的 密码,最后会使用base64编码为字符串存储在配置文件中 +// 不能出现任何一个重复的byte位,必须又 0-255 组成,并且都需要包含 +func RandPassword() *Password { + // 随机生成一个由 0~255 组成的 byte 数组 + intArr := rand.Perm(PasswordLength) + password := &Password{} + for i, v := range intArr { + password[i] = byte(v) + if i == v { + // 确保不会出现如何一个byte位出现重复 + return RandPassword() + } + } + return password +} diff --git a/core/password_test.go b/core/password_test.go new file mode 100755 index 0000000..3c13776 --- /dev/null +++ b/core/password_test.go @@ -0,0 +1,43 @@ +package core + +import ( + "reflect" + "sort" + "testing" +) + +func (password *Password) Len() int { + return PasswordLength +} + +func (password *Password) Less(i, j int) bool { + return password[i] < password[j] +} + +func (password *Password) Swap(i, j int) { + password[i], password[j] = password[j], password[i] +} + +func TestRandPassword(t *testing.T) { + password := RandPassword() + t.Log(password) + sort.Sort(password) + for i := 0; i < PasswordLength; i++ { + if password[i] != byte(i) { + t.Error("不能出现任何一个重复的byte位,必须由 0-255 组成,并且都需要包含") + } + } +} + +func TestPasswordString(t *testing.T) { + password := RandPassword() + passwordStr := password.String() + decodePassword, err := ParsePassword(passwordStr) + if err != nil { + t.Error(err) + } else { + if !reflect.DeepEqual(password, decodePassword) { + t.Error("密码转化成字符串后反解后数据不对应") + } + } +} diff --git a/core/securesocket.go b/core/securesocket.go new file mode 100755 index 0000000..c952128 --- /dev/null +++ b/core/securesocket.go @@ -0,0 +1,92 @@ +package core + +import ( + "errors" + "fmt" + "io" + "net" +) + +const ( + BufSize = 1024 +) + +// 加密传输的 TCP Socket +type SecureSocket struct { + Cipher *Cipher + ListenAddr *net.TCPAddr + RemoteAddr *net.TCPAddr +} + +// 从输入流里读取加密过的数据,解密后把原数据放到bs里 +func (secureSocket *SecureSocket) DecodeRead(conn *net.TCPConn, bs []byte) (n int, err error) { + n, err = conn.Read(bs) + if err != nil { + return + } + secureSocket.Cipher.decode(bs[:n]) + return +} + +// 把放在bs里的数据加密后立即全部写入输出流 +func (secureSocket *SecureSocket) EncodeWrite(conn *net.TCPConn, bs []byte) (int, error) { + secureSocket.Cipher.encode(bs) + return conn.Write(bs) +} + +// 从src中源源不断的读取原数据加密后写入到dst,直到src中没有数据可以再读取 +func (secureSocket *SecureSocket) EncodeCopy(dst *net.TCPConn, src *net.TCPConn) error { + buf := make([]byte, BufSize) + for { + readCount, errRead := src.Read(buf) + if errRead != nil { + if errRead != io.EOF { + return errRead + } else { + return nil + } + } + if readCount > 0 { + writeCount, errWrite := secureSocket.EncodeWrite(dst, buf[0:readCount]) + if errWrite != nil { + return errWrite + } + if readCount != writeCount { + return io.ErrShortWrite + } + } + } +} + +// 从src中源源不断的读取加密后的数据解密后写入到dst,直到src中没有数据可以再读取 +func (secureSocket *SecureSocket) DecodeCopy(dst *net.TCPConn, src *net.TCPConn) error { + buf := make([]byte, BufSize) + for { + readCount, errRead := secureSocket.DecodeRead(src, buf) + if errRead != nil { + if errRead != io.EOF { + return errRead + } else { + return nil + } + } + if readCount > 0 { + writeCount, errWrite := dst.Write(buf[0:readCount]) + if errWrite != nil { + return errWrite + } + if readCount != writeCount { + return io.ErrShortWrite + } + } + } +} + +// 和远程的socket建立连接,他们之间的数据传输会加密 +func (secureSocket *SecureSocket) DialRemote() (*net.TCPConn, error) { + remoteConn, err := net.DialTCP("tcp", nil, secureSocket.RemoteAddr) + if err != nil { + return nil, errors.New(fmt.Sprintf("连接到远程服务器 %s 失败:%s", secureSocket.RemoteAddr, err)) + } + return remoteConn, nil +} diff --git a/goreleaser.yml b/goreleaser.yml new file mode 100755 index 0000000..47a8fba --- /dev/null +++ b/goreleaser.yml @@ -0,0 +1,21 @@ +builds: + - binary: lightsocks-client + main: client.go + goos: + - windows + - darwin + - linux + goarch: + - amd64 + - 386 + - arm + - binary: lightsocks-server + main: server.go + goos: + - windows + - darwin + - linux + goarch: + - amd64 + - 386 + - arm diff --git a/local/.DS_Store b/local/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/local/.DS_Store differ diff --git a/local/local.go b/local/local.go new file mode 100755 index 0000000..6f474c0 --- /dev/null +++ b/local/local.go @@ -0,0 +1,80 @@ +package local + +import ( + "log" + "net" + + "../core" +) + +type LsLocal struct { + *core.SecureSocket +} + +// 新建一个本地端 +// 本地端的职责是: +// 1. 监听来自本机浏览器的代理请求 +// 2. 转发前加密数据 +// 3. 转发socket数据到墙外代理服务端 +// 4. 把服务端返回的数据转发给用户的浏览器 +func New(password *core.Password, listenAddr, remoteAddr *net.TCPAddr) *LsLocal { + return &LsLocal{ + SecureSocket: &core.SecureSocket{ + Cipher: core.NewCipher(password), + ListenAddr: listenAddr, + RemoteAddr: remoteAddr, + }, + } +} + +// 本地端启动监听,接收来自本机浏览器的连接 +func (local *LsLocal) Listen(didListen func(listenAddr net.Addr)) error { + listener, err := net.ListenTCP("tcp", local.ListenAddr) + if err != nil { + return err + } + + defer listener.Close() + + if didListen != nil { + didListen(listener.Addr()) + } + + for { + userConn, err := listener.AcceptTCP() + if err != nil { + log.Println(err) + continue + } + // userConn被关闭时直接清除所有数据 不管没有发送的数据 + userConn.SetLinger(0) + go local.handleConn(userConn) + } + return nil +} + +func (local *LsLocal) handleConn(userConn *net.TCPConn) { + defer userConn.Close() + + proxyServer, err := local.DialRemote() + if err != nil { + log.Println(err) + return + } + defer proxyServer.Close() + // Conn被关闭时直接清除所有数据 不管没有发送的数据 + proxyServer.SetLinger(0) + + // 进行转发 + // 从 proxyServer 读取数据发送到 localUser + go func() { + err := local.DecodeCopy(userConn, proxyServer) + if err != nil { + // 在 copy 的过程中可能会存在网络超时等 error 被 return,只要有一个发生了错误就退出本次工作 + userConn.Close() + proxyServer.Close() + } + }() + // 从 localUser 发送数据发送到 proxyServer,这里因为处在翻墙阶段出现网络错误的概率更大 + local.EncodeCopy(proxyServer, userConn) +} diff --git a/server.go b/server.go new file mode 100755 index 0000000..721b4be --- /dev/null +++ b/server.go @@ -0,0 +1,58 @@ +//SERVER + +package main + +import ( + "fmt" + "log" + "net" + + "./cmd" + "./core" + "./server" + + "github.com/phayes/freeport" +) + +var version = "master" + +func main() { + fmt.Print("Ich*Liebe*Dich~ \n", "SERVER\n") + log.SetFlags(log.Lshortfile) + + // 服务端监听端口随机生成 + port, err := freeport.GetFreePort() + if err != nil { + // 随机端口失败就采用 7448 + port = 7448 + } + // 默认配置 + config := &cmd.Config{ + ListenAddr: fmt.Sprintf(":%d", port), + // 密码随机生成 + Password: core.RandPassword().String(), + } + config.ReadConfig() + + // 解析配置 + password, err := core.ParsePassword(config.Password) + if err != nil { + log.Fatalln(err) + } + listenAddr, err := net.ResolveTCPAddr("tcp", config.ListenAddr) + if err != nil { + log.Fatalln(err) + } + + // 启动 server 端并监听 + lsServer := server.New(password, listenAddr) + log.Fatalln(lsServer.Listen(func(listenAddr net.Addr) { + log.Println("使用配置:", fmt.Sprintf(` +本地监听地址 listen: +%s +密码 password: +%s + `, listenAddr, password)) + log.Printf("lightsocks-server:%s 启动成功 监听在 %s\n", version, listenAddr.String()) + })) +} diff --git a/server/.DS_Store b/server/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/server/.DS_Store differ diff --git a/server/server.go b/server/server.go new file mode 100755 index 0000000..360d1e8 --- /dev/null +++ b/server/server.go @@ -0,0 +1,173 @@ +package server + +import ( + "encoding/binary" + "log" + "net" + + "../core" +) + +type LsServer struct { + *core.SecureSocket +} + +// New 新建一个服务端 +// 服务端的职责是: +// 1. 监听来自本地代理客户端的请求 +// 2. 解密本地代理客户端请求的数据,解析 SOCKS5 协议,连接用户浏览器真正想要连接的远程服务器 +// 3. 转发用户浏览器真正想要连接的远程服务器返回的数据的加密后的内容到本地代理客户端 +func New(password *core.Password, listenAddr *net.TCPAddr) *LsServer { + return &LsServer{ + SecureSocket: &core.SecureSocket{ + Cipher: core.NewCipher(password), + ListenAddr: listenAddr, + }, + } +} + +// Listen 运行服务端并且监听来自本地代理客户端的请求 +func (lsServer *LsServer) Listen(didListen func(listenAddr net.Addr)) error { + listener, err := net.ListenTCP("tcp", lsServer.ListenAddr) + if err != nil { + return err + } + + defer listener.Close() + + if didListen != nil { + didListen(listener.Addr()) + } + + for { + localConn, err := listener.AcceptTCP() + if err != nil { + log.Println(err) + continue + } + // localConn被关闭时直接清除所有数据 不管没有发送的数据 + localConn.SetLinger(0) + go lsServer.handleConn(localConn) + } + return nil +} + +// 解 SOCKS5 协议 +// https://www.ietf.org/rfc/rfc1928.txt +func (lsServer *LsServer) handleConn(localConn *net.TCPConn) { + defer localConn.Close() + buf := make([]byte, 256) + + /** + The localConn connects to the dstServer, and sends a ver + identifier/method selection message: + +----+----------+----------+ + |VER | NMETHODS | METHODS | + +----+----------+----------+ + | 1 | 1 | 1 to 255 | + +----+----------+----------+ + The VER field is set to X'05' for this ver of the protocol. The + NMETHODS field contains the number of method identifier octets that + appear in the METHODS field. + */ + // 第一个字段VER代表Socks的版本,Socks5默认为0x05,其固定长度为1个字节 + _, err := lsServer.DecodeRead(localConn, buf) + // 只支持版本5 + if err != nil || buf[0] != 0x05 { + return + } + + /** + The dstServer selects from one of the methods given in METHODS, and + sends a METHOD selection message: + + +----+--------+ + |VER | METHOD | + +----+--------+ + | 1 | 1 | + +----+--------+ + */ + // 不需要验证,直接验证通过 + lsServer.EncodeWrite(localConn, []byte{0x05, 0x00}) + + /** + +----+-----+-------+------+----------+----------+ + |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + +----+-----+-------+------+----------+----------+ + | 1 | 1 | X'00' | 1 | Variable | 2 | + +----+-----+-------+------+----------+----------+ + */ + + // 获取真正的远程服务的地址 + n, err := lsServer.DecodeRead(localConn, buf) + // n 最短的长度为7 情况为 ATYP=3 DST.ADDR占用1字节 值为0x0 + if err != nil || n < 7 { + return + } + + // CMD代表客户端请求的类型,值长度也是1个字节,有三种类型 + // CONNECT X'01' + if buf[1] != 0x01 { + // 目前只支持 CONNECT + return + } + + var dIP []byte + // aType 代表请求的远程服务器地址类型,值长度1个字节,有三种类型 + switch buf[3] { + case 0x01: + // IP V4 address: X'01' + dIP = buf[4 : 4+net.IPv4len] + case 0x03: + // DOMAINNAME: X'03' + ipAddr, err := net.ResolveIPAddr("ip", string(buf[5:n-2])) + if err != nil { + return + } + dIP = ipAddr.IP + case 0x04: + // IP V6 address: X'04' + dIP = buf[4 : 4+net.IPv6len] + default: + return + } + dPort := buf[n-2:] + dstAddr := &net.TCPAddr{ + IP: dIP, + Port: int(binary.BigEndian.Uint16(dPort)), + } + + // 连接真正的远程服务 + dstServer, err := net.DialTCP("tcp", nil, dstAddr) + if err != nil { + return + } else { + defer dstServer.Close() + // Conn被关闭时直接清除所有数据 不管没有发送的数据 + dstServer.SetLinger(0) + + // 响应客户端连接成功 + /** + +----+-----+-------+------+----------+----------+ + |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + +----+-----+-------+------+----------+----------+ + | 1 | 1 | X'00' | 1 | Variable | 2 | + +----+-----+-------+------+----------+----------+ + */ + // 响应客户端连接成功 + lsServer.EncodeWrite(localConn, []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + } + + // 进行转发 + // 从 localUser 读取数据发送到 dstServer + go func() { + err := lsServer.DecodeCopy(dstServer, localConn) + if err != nil { + // 在 copy 的过程中可能会存在网络超时等 error 被 return,只要有一个发生了错误就退出本次工作 + localConn.Close() + dstServer.Close() + } + }() + // 从 dstServer 读取数据发送到 localUser,这里因为处在翻墙阶段出现网络错误的概率更大 + lsServer.EncodeCopy(localConn, dstServer) +}