dae/component/outbound/dialer/trojan/trojan.go
2023-01-23 19:54:21 +08:00

233 lines
6.2 KiB
Go

package trojan
import (
"fmt"
"foo/common"
"foo/component/outbound/dialer"
"foo/component/outbound/dialer/transport/tls"
"foo/component/outbound/dialer/transport/ws"
"github.com/mzz2017/softwind/protocol"
"github.com/mzz2017/softwind/transport/grpc"
"gopkg.in/yaml.v3"
"net"
"net/url"
"strconv"
"strings"
)
func init() {
dialer.FromLinkRegister("trojan", NewTrojan)
dialer.FromLinkRegister("trojan-go", NewTrojan)
dialer.FromClashRegister("trojan", NewTrojanFromClashObj)
}
type Trojan struct {
Name string `json:"name"`
Server string `json:"server"`
Port int `json:"port"`
Password string `json:"password"`
Sni string `json:"sni"`
Type string `json:"type"`
Encryption string `json:"encryption"`
Host string `json:"host"`
Path string `json:"path"`
ServiceName string `json:"serviceName"`
AllowInsecure bool `json:"allowInsecure"`
Protocol string `json:"protocol"`
}
func NewTrojan(link string) (*dialer.Dialer, error) {
s, err := ParseTrojanURL(link)
if err != nil {
return nil, err
}
return s.Dialer()
}
func NewTrojanFromClashObj(o *yaml.Node) (*dialer.Dialer, error) {
s, err := ParseClash(o)
if err != nil {
return nil, err
}
return s.Dialer()
}
func (s *Trojan) Dialer() (*dialer.Dialer, error) {
d := dialer.SymmetricDirect
u := url.URL{
Scheme: "tls",
Host: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
RawQuery: url.Values{
"sni": []string{s.Sni},
}.Encode(),
}
var err error
if s.Type != "grpc" {
// grpc contains tls
if d, err = tls.NewTls(u.String(), d); err != nil {
return nil, err
}
}
// "tls,ws,ss,trojanc"
switch s.Type {
case "ws":
u = url.URL{
Scheme: "ws",
Host: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
RawQuery: url.Values{
"host": []string{s.Host},
"path": []string{s.Path},
}.Encode(),
}
if d, err = ws.NewWs(u.String(), d); err != nil {
return nil, err
}
case "grpc":
serviceName := s.ServiceName
if serviceName == "" {
serviceName = "GunService"
}
d = &grpc.Dialer{
NextDialer: &protocol.DialerConverter{Dialer: d},
ServiceName: serviceName,
ServerName: s.Sni,
}
}
if strings.HasPrefix(s.Encryption, "ss;") {
fields := strings.SplitN(s.Encryption, ";", 3)
if d, err = protocol.NewDialer("shadowsocks", d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Cipher: fields[1],
Password: fields[2],
IsClient: false,
}); err != nil {
return nil, err
}
}
if d, err = protocol.NewDialer("trojanc", d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Password: s.Password,
IsClient: true,
}); err != nil {
return nil, err
}
return dialer.NewDialer(d, true, s.Name, s.Protocol, s.ExportToURL()), nil
}
func ParseTrojanURL(u string) (data *Trojan, err error) {
//trojan://password@server:port#escape(remarks)
t, err := url.Parse(u)
if err != nil {
err = fmt.Errorf("invalid trojan format")
return
}
allowInsecure := t.Query().Get("allowInsecure")
sni := t.Query().Get("peer")
if sni == "" {
sni = t.Query().Get("sni")
}
if sni == "" {
sni = t.Hostname()
}
port, err := strconv.Atoi(t.Port())
if err != nil {
return nil, dialer.InvalidParameterErr
}
data = &Trojan{
Name: t.Fragment,
Server: t.Hostname(),
Port: port,
Password: t.User.Username(),
Sni: sni,
AllowInsecure: allowInsecure == "1" || allowInsecure == "true",
Protocol: "trojan",
}
if t.Query().Get("type") != "" {
t.Scheme = "trojan-go"
}
if t.Scheme == "trojan-go" {
data.Protocol = "trojan-go"
data.Encryption = t.Query().Get("encryption")
data.Host = t.Query().Get("host")
data.Path = t.Query().Get("path")
data.Type = t.Query().Get("type")
data.ServiceName = t.Query().Get("serviceName")
if data.Type == "grpc" && data.ServiceName == "" {
data.ServiceName = data.Path
}
data.AllowInsecure = false
}
return data, nil
}
func ParseClash(o *yaml.Node) (data *Trojan, err error) {
type WSOptions struct {
Path string `yaml:"path,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
MaxEarlyData int `yaml:"max-early-data,omitempty"`
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
}
type GrpcOptions struct {
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
}
type TrojanOption struct {
Name string `yaml:"name"`
Server string `yaml:"server"`
Port int `yaml:"port"`
Password string `yaml:"password"`
ALPN []string `yaml:"alpn,omitempty"`
SNI string `yaml:"sni,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
UDP bool `yaml:"udp,omitempty"`
Network string `yaml:"network,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
}
var option TrojanOption
if err = o.Decode(&option); err != nil {
return nil, err
}
proto := "trojan"
if option.Network != "" && option.Network != "origin" {
proto = "trojan-go"
}
return &Trojan{
Name: option.Name,
Server: option.Server,
Port: option.Port,
Password: option.Password,
Sni: option.SNI,
Type: option.Network,
Encryption: "",
Host: option.WSOpts.Headers["Host"],
Path: option.WSOpts.Path,
AllowInsecure: option.SkipCertVerify,
ServiceName: option.GrpcOpts.GrpcServiceName,
Protocol: proto,
}, nil
}
func (t *Trojan) ExportToURL() string {
u := &url.URL{
Scheme: "trojan",
User: url.User(t.Password),
Host: net.JoinHostPort(t.Server, strconv.Itoa(t.Port)),
Fragment: t.Name,
}
q := u.Query()
if t.AllowInsecure {
q.Set("allowInsecure", "1")
}
common.SetValue(&q, "sni", t.Sni)
if t.Protocol == "trojan-go" {
u.Scheme = "trojan-go"
common.SetValue(&q, "host", t.Host)
common.SetValue(&q, "encryption", t.Encryption)
common.SetValue(&q, "type", t.Type)
common.SetValue(&q, "path", t.Path)
}
u.RawQuery = q.Encode()
return u.String()
}