diff --git a/README.md b/README.md index ac50c862..5b63e8f6 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ GoDoc at https://godoc.org/github.com/shadowsocks/go-shadowsocks2/ - [x] Support for Netfilter TCP redirect (IPv6 should work but not tested) - [x] UDP tunneling (e.g. relay DNS packets) - [x] TCP tunneling (e.g. benchmark with iperf3) +- [x] SIP003 plugins ## Install @@ -95,6 +96,28 @@ Start iperf3 client to connect to the tunneld port instead iperf3 -c localhost -p 1090 ``` +### SIP003 Plugins (Experimental) + +Both client and server support SIP003 plugins. +Use `-plugin` and `-plugin-opts` parameters to enable. + +Client: + +```sh +shadowsocks2 -c 'ss://AEAD_CHACHA20_POLY1305:your-password@[server_address]:8488' \ + -verbose -socks :1080 -u -plugin v2ray +``` +Server: + +```sh +shadowsocks2 -s 'ss://AEAD_CHACHA20_POLY1305:your-password@:8488' -verbose \ + -plugin v2ray -plugin-opts "server" +``` +Note: + +It will look for the plugin in the current directory first, then `$PATH`. + +UDP connections will not be affected by SIP003. ## Design Principles diff --git a/log.go b/log.go new file mode 100644 index 00000000..7648db61 --- /dev/null +++ b/log.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "log" + "os" +) + +var logger = log.New(os.Stderr, "", log.Lshortfile|log.LstdFlags) + +func logf(f string, v ...interface{}) { + if config.Verbose { + logger.Output(2, fmt.Sprintf(f, v...)) + } +} + +type logHelper struct { + prefix string +} + +func (l *logHelper) Write(p []byte) (n int, err error) { + if config.Verbose { + logger.Printf("%s%s\n", l.prefix, p) + return len(p), nil + } + return +} + +func newLogHelper(prefix string) *logHelper { + return &logHelper{prefix} +} diff --git a/main.go b/main.go index a2894b5e..e1a7a428 100644 --- a/main.go +++ b/main.go @@ -23,29 +23,23 @@ var config struct { UDPTimeout time.Duration } -var logger = log.New(os.Stderr, "", log.Lshortfile|log.LstdFlags) - -func logf(f string, v ...interface{}) { - if config.Verbose { - logger.Output(2, fmt.Sprintf(f, v...)) - } -} - func main() { var flags struct { - Client string - Server string - Cipher string - Key string - Password string - Keygen int - Socks string - RedirTCP string - RedirTCP6 string - TCPTun string - UDPTun string - UDPSocks bool + Client string + Server string + Cipher string + Key string + Password string + Keygen int + Socks string + RedirTCP string + RedirTCP6 string + TCPTun string + UDPTun string + UDPSocks bool + Plugin string + PluginOpts string } flag.BoolVar(&config.Verbose, "verbose", false, "verbose mode") @@ -61,6 +55,8 @@ func main() { flag.StringVar(&flags.RedirTCP6, "redir6", "", "(client-only) redirect TCP IPv6 from this address") flag.StringVar(&flags.TCPTun, "tcptun", "", "(client-only) TCP tunnel (laddr1=raddr1,laddr2=raddr2,...)") flag.StringVar(&flags.UDPTun, "udptun", "", "(client-only) UDP tunnel (laddr1=raddr1,laddr2=raddr2,...)") + flag.StringVar(&flags.Plugin, "plugin", "", "Enable SIP003 plugin. (e.g., v2ray-plugin)") + flag.StringVar(&flags.PluginOpts, "plugin-opts", "", "Set SIP003 plugin options. (e.g., \"server;tls;host=mydomain.me\")") flag.DurationVar(&config.UDPTimeout, "udptimeout", 5*time.Minute, "UDP tunnel timeout") flag.Parse() @@ -98,15 +94,24 @@ func main() { } } + udpAddr := addr + ciph, err := core.PickCipher(cipher, key, password) if err != nil { log.Fatal(err) } + if flags.Plugin != "" { + addr, err = startPlugin(flags.Plugin, flags.PluginOpts, addr, false) + if err != nil { + log.Fatal(err) + } + } + if flags.UDPTun != "" { for _, tun := range strings.Split(flags.UDPTun, ",") { p := strings.Split(tun, "=") - go udpLocal(p[0], addr, p[1], ciph.PacketConn) + go udpLocal(p[0], udpAddr, p[1], ciph.PacketConn) } } @@ -121,7 +126,7 @@ func main() { socks.UDPEnabled = flags.UDPSocks go socksLocal(flags.Socks, addr, ciph.StreamConn) if flags.UDPSocks { - go udpSocksLocal(flags.Socks, addr, ciph.PacketConn) + go udpSocksLocal(flags.Socks, udpAddr, ciph.PacketConn) } } @@ -147,12 +152,21 @@ func main() { } } + udpAddr := addr + + if flags.Plugin != "" { + addr, err = startPlugin(flags.Plugin, flags.PluginOpts, addr, true) + if err != nil { + log.Fatal(err) + } + } + ciph, err := core.PickCipher(cipher, key, password) if err != nil { log.Fatal(err) } - go udpRemote(addr, ciph.PacketConn) + go udpRemote(udpAddr, ciph.PacketConn) go tcpRemote(addr, ciph.StreamConn) } diff --git a/plugin.go b/plugin.go new file mode 100644 index 00000000..0d2ac420 --- /dev/null +++ b/plugin.go @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "net" + "os" + "os/exec" +) + +func startPlugin(plugin, pluginOpts, ssAddr string, isServer bool) (newAddr string, err error) { + logf("starting plugin (%s) with option (%s)....", plugin, pluginOpts) + freePort, err := getFreePort() + if err != nil { + return "", fmt.Errorf("failed to fetch an unused port for plugin (%v)", err) + } + localHost := "127.0.0.1" + ssHost, ssPort, err := net.SplitHostPort(ssAddr) + if err != nil { + return "", err + } + newAddr = localHost + ":" + freePort + if isServer { + if ssHost == "" { + ssHost = "0.0.0.0" + } + logf("plugin (%s) will listen on %s:%s", plugin, ssHost, ssPort) + } else { + logf("plugin (%s) will listen on %s:%s", plugin, localHost, freePort) + } + err = execPlugin(plugin, pluginOpts, ssHost, ssPort, localHost, freePort) + return +} + +func execPlugin(plugin, pluginOpts, remoteHost, remotePort, localHost, localPort string) error { + if fileExists(plugin) { + plugin = "./" + plugin + } + logH := newLogHelper("[" + plugin + "]: ") + env := append(os.Environ(), + "SS_REMOTE_HOST="+remoteHost, + "SS_REMOTE_PORT="+remotePort, + "SS_LOCAL_HOST="+localHost, + "SS_LOCAL_PORT="+localPort, + "SS_PLUGIN_OPTIONS="+pluginOpts, + ) + cmd := &exec.Cmd{ + Path: plugin, + Args: []string{plugin}, + Env: env, + Stdout: logH, + Stderr: logH, + } + if err := cmd.Start(); err != nil { + return err + } + go func() { + if err := cmd.Wait(); err != nil { + logf("plugin exited (%v)\n", err) + os.Exit(2) + } + logf("plugin exited\n") + os.Exit(0) + }() + return nil +} + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func getFreePort() (string, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return "", err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return "", err + } + port := fmt.Sprintf("%d", l.Addr().(*net.TCPAddr).Port) + l.Close() + return port, nil +}