Add obfs-local and v2ray-plugin support for shadowsocks outbound
This commit is contained in:
119
transport/sip003/args.go
Normal file
119
transport/sip003/args.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package sip003
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// mod from https://github.com/shadowsocks/v2ray-plugin/blob/master/args.go
|
||||
|
||||
// Args maps a string key to a list of values. It is similar to url.Values.
|
||||
type Args map[string][]string
|
||||
|
||||
// Get the first value associated with the given key. If there are any values
|
||||
// associated with the key, the value return has the value and ok is set to
|
||||
// true. If there are no values for the given key, value is "" and ok is false.
|
||||
// If you need access to multiple values, use the map directly.
|
||||
func (args Args) Get(key string) (value string, ok bool) {
|
||||
if args == nil {
|
||||
return "", false
|
||||
}
|
||||
vals, ok := args[key]
|
||||
if !ok || len(vals) == 0 {
|
||||
return "", false
|
||||
}
|
||||
return vals[0], true
|
||||
}
|
||||
|
||||
// Add Append value to the list of values for key.
|
||||
func (args Args) Add(key, value string) {
|
||||
args[key] = append(args[key], value)
|
||||
}
|
||||
|
||||
// Return the index of the next unescaped byte in s that is in the term set, or
|
||||
// else the length of the string if no terminators appear. Additionally return
|
||||
// the unescaped string up to the returned index.
|
||||
func indexUnescaped(s string, term []byte) (int, string, error) {
|
||||
var i int
|
||||
unesc := make([]byte, 0)
|
||||
for i = 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
// A terminator byte?
|
||||
if bytes.IndexByte(term, b) != -1 {
|
||||
break
|
||||
}
|
||||
if b == '\\' {
|
||||
i++
|
||||
if i >= len(s) {
|
||||
return 0, "", fmt.Errorf("nothing following final escape in %q", s)
|
||||
}
|
||||
b = s[i]
|
||||
}
|
||||
unesc = append(unesc, b)
|
||||
}
|
||||
return i, string(unesc), nil
|
||||
}
|
||||
|
||||
// ParsePluginOptions Parse a name–value mapping as from SS_PLUGIN_OPTIONS.
|
||||
//
|
||||
// "<value> is a k=v string value with options that are to be passed to the
|
||||
// transport. semicolons, equal signs and backslashes must be escaped
|
||||
// with a backslash."
|
||||
// Example: secret=nou;cache=/tmp/cache;secret=yes
|
||||
func ParsePluginOptions(s string) (opts Args, err error) {
|
||||
opts = make(Args)
|
||||
if len(s) == 0 {
|
||||
return
|
||||
}
|
||||
i := 0
|
||||
for {
|
||||
var key, value string
|
||||
var offset, begin int
|
||||
|
||||
if i >= len(s) {
|
||||
break
|
||||
}
|
||||
begin = i
|
||||
// Read the key.
|
||||
offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(key) == 0 {
|
||||
err = fmt.Errorf("empty key in %q", s[begin:i])
|
||||
return
|
||||
}
|
||||
i += offset
|
||||
// End of string or no equals sign?
|
||||
if i >= len(s) || s[i] != '=' {
|
||||
opts.Add(key, "1")
|
||||
// Skip the semicolon.
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// Skip the equals sign.
|
||||
i++
|
||||
// Read the value.
|
||||
offset, value, err = indexUnescaped(s[i:], []byte{';'})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
i += offset
|
||||
opts.Add(key, value)
|
||||
// Skip the semicolon.
|
||||
i++
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// Escape backslashes and all the bytes that are in set.
|
||||
func backslashEscape(s string, set []byte) string {
|
||||
var buf bytes.Buffer
|
||||
for _, b := range []byte(s) {
|
||||
if b == '\\' || bytes.IndexByte(set, b) != -1 {
|
||||
buf.WriteByte('\\')
|
||||
}
|
||||
buf.WriteByte(b)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
59
transport/sip003/obfs.go
Normal file
59
transport/sip003/obfs.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package sip003
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/transport/simple-obfs"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
var _ Plugin = (*ObfsLocal)(nil)
|
||||
|
||||
func init() {
|
||||
RegisterPlugin("obfs-local", newObfsLocal)
|
||||
}
|
||||
|
||||
func newObfsLocal(pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
|
||||
var plugin ObfsLocal
|
||||
mode := "http"
|
||||
if obfsMode, loaded := pluginOpts.Get("obfs"); loaded {
|
||||
mode = obfsMode
|
||||
}
|
||||
if obfsHost, loaded := pluginOpts.Get("obfs-host"); loaded {
|
||||
plugin.host = obfsHost
|
||||
}
|
||||
switch mode {
|
||||
case "http":
|
||||
case "tls":
|
||||
plugin.tls = true
|
||||
default:
|
||||
return nil, E.New("unknown obfs mode ", mode)
|
||||
}
|
||||
plugin.port = F.ToString(serverAddr.Port)
|
||||
return &plugin, nil
|
||||
}
|
||||
|
||||
type ObfsLocal struct {
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
tls bool
|
||||
host string
|
||||
port string
|
||||
}
|
||||
|
||||
func (o *ObfsLocal) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
conn, err := o.dialer.DialContext(ctx, N.NetworkTCP, o.serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !o.tls {
|
||||
return obfs.NewHTTPObfs(conn, o.host, o.port), nil
|
||||
} else {
|
||||
return obfs.NewTLSObfs(conn, o.host), nil
|
||||
}
|
||||
}
|
||||
35
transport/sip003/plugin.go
Normal file
35
transport/sip003/plugin.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package sip003
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type PluginConstructor func(pluginArgs Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error)
|
||||
|
||||
type Plugin interface {
|
||||
DialContext(ctx context.Context) (net.Conn, error)
|
||||
}
|
||||
|
||||
var plugins map[string]PluginConstructor
|
||||
|
||||
func RegisterPlugin(name string, constructor PluginConstructor) {
|
||||
plugins[name] = constructor
|
||||
}
|
||||
|
||||
func CreatePlugin(name string, pluginArgs string, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
|
||||
pluginOptions, err := ParsePluginOptions(pluginArgs)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse plugin_opts")
|
||||
}
|
||||
constructor, loaded := plugins[name]
|
||||
if !loaded {
|
||||
return nil, E.New("plugin not found: ", name)
|
||||
}
|
||||
return constructor(pluginOptions, router, dialer, serverAddr)
|
||||
}
|
||||
80
transport/sip003/v2ray.go
Normal file
80
transport/sip003/v2ray.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package sip003
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/transport/v2ray"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterPlugin("v2ray-plugin", newV2RayPlugin)
|
||||
}
|
||||
|
||||
func newV2RayPlugin(pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
|
||||
var tlsOptions option.OutboundTLSOptions
|
||||
if _, loaded := pluginOpts.Get("tls"); loaded {
|
||||
tlsOptions.Enabled = true
|
||||
}
|
||||
if certPath, certLoaded := pluginOpts.Get("cert"); certLoaded {
|
||||
tlsOptions.CertificatePath = certPath
|
||||
}
|
||||
if certRaw, certLoaded := pluginOpts.Get("certRaw"); certLoaded {
|
||||
certHead := "-----BEGIN CERTIFICATE-----"
|
||||
certTail := "-----END CERTIFICATE-----"
|
||||
fixedCert := certHead + "\n" + certRaw + "\n" + certTail
|
||||
tlsOptions.Certificate = fixedCert
|
||||
}
|
||||
|
||||
mode := "websocket"
|
||||
if modeOpt, loaded := pluginOpts.Get("mode"); loaded {
|
||||
mode = modeOpt
|
||||
}
|
||||
|
||||
host := "cloudfront.com"
|
||||
path := "/"
|
||||
|
||||
if hostOpt, loaded := pluginOpts.Get("host"); loaded {
|
||||
host = hostOpt
|
||||
}
|
||||
if pathOpt, loaded := pluginOpts.Get("path"); loaded {
|
||||
path = pathOpt
|
||||
}
|
||||
|
||||
var tlsClient tls.Config
|
||||
var err error
|
||||
if tlsOptions.Enabled {
|
||||
tlsClient, err = tls.NewClient(router, serverAddr.AddrString(), tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var transportOptions option.V2RayTransportOptions
|
||||
switch mode {
|
||||
case "websocket":
|
||||
transportOptions = option.V2RayTransportOptions{
|
||||
Type: C.V2RayTransportTypeWebsocket,
|
||||
WebsocketOptions: option.V2RayWebsocketOptions{
|
||||
Headers: map[string]string{
|
||||
"Host": host,
|
||||
},
|
||||
Path: path,
|
||||
},
|
||||
}
|
||||
case "quic":
|
||||
transportOptions = option.V2RayTransportOptions{
|
||||
Type: C.V2RayTransportTypeQUIC,
|
||||
}
|
||||
default:
|
||||
return nil, E.New("v2ray-plugin: unknown mode: " + mode)
|
||||
}
|
||||
|
||||
return v2ray.NewClientTransport(context.Background(), dialer, serverAddr, transportOptions, tlsClient)
|
||||
}
|
||||
Reference in New Issue
Block a user