mirror of
https://github.com/p4gefau1t/trojan-go.git
synced 2024-04-21 12:21:34 +00:00
460 lines
12 KiB
Go
460 lines
12 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
v2router "github.com/v2fly/v2ray-core/v4/app/router"
|
|
|
|
"github.com/p4gefau1t/trojan-go/common"
|
|
"github.com/p4gefau1t/trojan-go/common/geodata"
|
|
"github.com/p4gefau1t/trojan-go/config"
|
|
"github.com/p4gefau1t/trojan-go/log"
|
|
"github.com/p4gefau1t/trojan-go/tunnel"
|
|
"github.com/p4gefau1t/trojan-go/tunnel/freedom"
|
|
"github.com/p4gefau1t/trojan-go/tunnel/transport"
|
|
)
|
|
|
|
const (
|
|
Block = 0
|
|
Bypass = 1
|
|
Proxy = 2
|
|
)
|
|
|
|
const (
|
|
AsIs = 0
|
|
IPIfNonMatch = 1
|
|
IPOnDemand = 2
|
|
)
|
|
|
|
const MaxPacketSize = 1024 * 8
|
|
|
|
func matchDomain(list []*v2router.Domain, target string) bool {
|
|
for _, d := range list {
|
|
switch d.GetType() {
|
|
case v2router.Domain_Full:
|
|
domain := d.GetValue()
|
|
if domain == target {
|
|
log.Tracef("domain %s hit domain(full) rule: %s", target, domain)
|
|
return true
|
|
}
|
|
case v2router.Domain_Domain:
|
|
domain := d.GetValue()
|
|
if strings.HasSuffix(target, domain) {
|
|
idx := strings.Index(target, domain)
|
|
if idx == 0 || target[idx-1] == '.' {
|
|
log.Tracef("domain %s hit domain rule: %s", target, domain)
|
|
return true
|
|
}
|
|
}
|
|
case v2router.Domain_Plain:
|
|
//keyword
|
|
if strings.Contains(target, d.GetValue()) {
|
|
log.Tracef("domain %s hit keyword rule: %s", target, d.GetValue())
|
|
return true
|
|
}
|
|
case v2router.Domain_Regex:
|
|
matched, err := regexp.Match(d.GetValue(), []byte(target))
|
|
if err != nil {
|
|
log.Error("invalid regex", d.GetValue())
|
|
return false
|
|
}
|
|
if matched {
|
|
log.Tracef("domain %s hit regex rule: %s", target, d.GetValue())
|
|
return true
|
|
}
|
|
default:
|
|
log.Debug("unknown rule type:", d.GetType().String())
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchIP(list []*v2router.CIDR, target net.IP) bool {
|
|
isIPv6 := true
|
|
len := net.IPv6len
|
|
if target.To4() != nil {
|
|
len = net.IPv4len
|
|
isIPv6 = false
|
|
}
|
|
for _, c := range list {
|
|
n := int(c.GetPrefix())
|
|
mask := net.CIDRMask(n, 8*len)
|
|
cidrIP := net.IP(c.GetIp())
|
|
if cidrIP.To4() != nil { //IPv4 CIDR
|
|
if isIPv6 {
|
|
continue
|
|
}
|
|
} else { //IPv6 CIDR
|
|
if !isIPv6 {
|
|
continue
|
|
}
|
|
}
|
|
subnet := &net.IPNet{IP: cidrIP.Mask(mask), Mask: mask}
|
|
if subnet.Contains(target) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func newIPAddress(address *tunnel.Address) (*tunnel.Address, error) {
|
|
ip, err := address.ResolveIP()
|
|
if err != nil {
|
|
return nil, common.NewError("router failed to resolve ip").Base(err)
|
|
}
|
|
newAddress := &tunnel.Address{
|
|
IP: ip,
|
|
Port: address.Port,
|
|
}
|
|
if ip.To4() != nil {
|
|
newAddress.AddressType = tunnel.IPv4
|
|
} else {
|
|
newAddress.AddressType = tunnel.IPv6
|
|
}
|
|
return newAddress, nil
|
|
}
|
|
|
|
type Client struct {
|
|
domains [3][]*v2router.Domain
|
|
cidrs [3][]*v2router.CIDR
|
|
defaultPolicy int
|
|
domainStrategy int
|
|
underlay tunnel.Client
|
|
direct *freedom.Client
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func (c *Client) Route(address *tunnel.Address) int {
|
|
policy := -1
|
|
var err error
|
|
if c.domainStrategy == IPOnDemand {
|
|
address, err = newIPAddress(address)
|
|
if err != nil {
|
|
return c.defaultPolicy
|
|
}
|
|
}
|
|
if address.AddressType == tunnel.DomainName {
|
|
for i := 0; i < 3; i++ {
|
|
if matchDomain(c.domains[i], address.DomainName) {
|
|
policy = i
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
for i := 0; i < 3; i++ {
|
|
if matchIP(c.cidrs[i], address.IP) {
|
|
policy = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if policy == -1 && c.domainStrategy == IPIfNonMatch {
|
|
address, err = newIPAddress(address)
|
|
if err != nil {
|
|
return c.defaultPolicy
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
if matchIP(c.cidrs[i], address.IP) {
|
|
policy = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if policy == -1 {
|
|
policy = c.defaultPolicy
|
|
}
|
|
return policy
|
|
}
|
|
|
|
func (c *Client) DialConn(address *tunnel.Address, overlay tunnel.Tunnel) (tunnel.Conn, error) {
|
|
policy := c.Route(address)
|
|
switch policy {
|
|
case Proxy:
|
|
return c.underlay.DialConn(address, overlay)
|
|
case Block:
|
|
return nil, common.NewError("router blocked address: " + address.String())
|
|
case Bypass:
|
|
conn, err := c.direct.DialConn(address, &Tunnel{})
|
|
if err != nil {
|
|
return nil, common.NewError("router dial error").Base(err)
|
|
}
|
|
return &transport.Conn{
|
|
Conn: conn,
|
|
}, nil
|
|
}
|
|
panic("unknown policy")
|
|
}
|
|
|
|
func (c *Client) DialPacket(overlay tunnel.Tunnel) (tunnel.PacketConn, error) {
|
|
directConn, err := net.ListenPacket("udp", "")
|
|
if err != nil {
|
|
return nil, common.NewError("router failed to dial udp (direct)").Base(err)
|
|
}
|
|
proxy, err := c.underlay.DialPacket(overlay)
|
|
if err != nil {
|
|
return nil, common.NewError("router failed to dial udp (proxy)").Base(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(c.ctx)
|
|
conn := &PacketConn{
|
|
Client: c,
|
|
PacketConn: directConn,
|
|
proxy: proxy,
|
|
cancel: cancel,
|
|
ctx: ctx,
|
|
packetChan: make(chan *packetInfo, 16),
|
|
}
|
|
go conn.packetLoop()
|
|
return conn, nil
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
c.cancel()
|
|
return c.underlay.Close()
|
|
}
|
|
|
|
type codeInfo struct {
|
|
code string
|
|
strategy int
|
|
}
|
|
|
|
func loadCode(cfg *Config, prefix string) []codeInfo {
|
|
codes := []codeInfo{}
|
|
for _, s := range cfg.Router.Proxy {
|
|
if strings.HasPrefix(s, prefix) {
|
|
if left := s[len(prefix):]; len(left) > 0 {
|
|
codes = append(codes, codeInfo{
|
|
code: left,
|
|
strategy: Proxy,
|
|
})
|
|
} else {
|
|
log.Warn("invalid empty rule:", s)
|
|
}
|
|
}
|
|
}
|
|
for _, s := range cfg.Router.Bypass {
|
|
if strings.HasPrefix(s, prefix) {
|
|
if left := s[len(prefix):]; len(left) > 0 {
|
|
codes = append(codes, codeInfo{
|
|
code: left,
|
|
strategy: Bypass,
|
|
})
|
|
} else {
|
|
log.Warn("invalid empty rule:", s)
|
|
}
|
|
}
|
|
}
|
|
for _, s := range cfg.Router.Block {
|
|
if strings.HasPrefix(s, prefix) {
|
|
if left := s[len(prefix):]; len(left) > 0 {
|
|
codes = append(codes, codeInfo{
|
|
code: left,
|
|
strategy: Block,
|
|
})
|
|
} else {
|
|
log.Warn("invalid empty rule:", s)
|
|
}
|
|
}
|
|
}
|
|
return codes
|
|
}
|
|
|
|
func NewClient(ctx context.Context, underlay tunnel.Client) (*Client, error) {
|
|
m1 := runtime.MemStats{}
|
|
m2 := runtime.MemStats{}
|
|
m3 := runtime.MemStats{}
|
|
m4 := runtime.MemStats{}
|
|
|
|
cfg := config.FromContext(ctx, Name).(*Config)
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithCancel(ctx)
|
|
|
|
direct, err := freedom.NewClient(ctx, nil)
|
|
if err != nil {
|
|
cancel()
|
|
return nil, common.NewError("router failed to initialize raw client").Base(err)
|
|
}
|
|
|
|
client := &Client{
|
|
domains: [3][]*v2router.Domain{},
|
|
cidrs: [3][]*v2router.CIDR{},
|
|
underlay: underlay,
|
|
direct: direct,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
switch strings.ToLower(cfg.Router.DomainStrategy) {
|
|
case "as_is", "as-is", "asis":
|
|
client.domainStrategy = AsIs
|
|
case "ip_if_non_match", "ip-if-non-match", "ipifnonmatch":
|
|
client.domainStrategy = IPIfNonMatch
|
|
case "ip_on_demand", "ip-on-demand", "ipondemand":
|
|
client.domainStrategy = IPOnDemand
|
|
default:
|
|
return nil, common.NewError("unknown strategy: " + cfg.Router.DomainStrategy)
|
|
}
|
|
|
|
switch strings.ToLower(cfg.Router.DefaultPolicy) {
|
|
case "proxy":
|
|
client.defaultPolicy = Proxy
|
|
case "bypass":
|
|
client.defaultPolicy = Bypass
|
|
case "block":
|
|
client.defaultPolicy = Block
|
|
default:
|
|
return nil, common.NewError("unknown strategy: " + cfg.Router.DomainStrategy)
|
|
}
|
|
|
|
runtime.ReadMemStats(&m1)
|
|
|
|
geodataLoader := geodata.NewGeodataLoader()
|
|
|
|
ipCode := loadCode(cfg, "geoip:")
|
|
for _, c := range ipCode {
|
|
code := c.code
|
|
cidrs, err := geodataLoader.LoadGeoIP(code)
|
|
if err != nil {
|
|
log.Error(err)
|
|
} else {
|
|
log.Infof("geoip:%s loaded", code)
|
|
client.cidrs[c.strategy] = append(client.cidrs[c.strategy], cidrs...)
|
|
}
|
|
}
|
|
|
|
runtime.ReadMemStats(&m2)
|
|
|
|
siteCode := loadCode(cfg, "geosite:")
|
|
for _, c := range siteCode {
|
|
code := c.code
|
|
attrWanted := ""
|
|
// Test if user wants domains that have an attribute
|
|
if attrIdx := strings.Index(code, "@"); attrIdx > 0 {
|
|
if !strings.HasSuffix(code, "@") {
|
|
code = c.code[:attrIdx]
|
|
attrWanted = c.code[attrIdx+1:]
|
|
} else { // "geosite:google@" is invalid
|
|
log.Warnf("geosite:%s invalid", code)
|
|
continue
|
|
}
|
|
} else if attrIdx == 0 { // "geosite:@cn" is invalid
|
|
log.Warnf("geosite:%s invalid", code)
|
|
continue
|
|
}
|
|
|
|
domainList, err := geodataLoader.LoadGeoSite(code)
|
|
if err != nil {
|
|
log.Error(err)
|
|
} else {
|
|
found := false
|
|
if attrWanted != "" {
|
|
for _, domain := range domainList {
|
|
for _, attr := range domain.GetAttribute() {
|
|
if strings.EqualFold(attrWanted, attr.GetKey()) {
|
|
client.domains[c.strategy] = append(client.domains[c.strategy], domain)
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
client.domains[c.strategy] = append(client.domains[c.strategy], domainList...)
|
|
found = true
|
|
}
|
|
if found {
|
|
log.Infof("geosite:%s loaded", c.code)
|
|
} else {
|
|
log.Errorf("geosite:%s not found", c.code)
|
|
}
|
|
}
|
|
}
|
|
|
|
runtime.ReadMemStats(&m3)
|
|
|
|
domainInfo := loadCode(cfg, "domain:")
|
|
for _, info := range domainInfo {
|
|
client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{
|
|
Type: v2router.Domain_Domain,
|
|
Value: strings.ToLower(info.code),
|
|
Attribute: nil,
|
|
})
|
|
}
|
|
|
|
keywordInfo := loadCode(cfg, "keyword:")
|
|
for _, info := range keywordInfo {
|
|
client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{
|
|
Type: v2router.Domain_Plain,
|
|
Value: strings.ToLower(info.code),
|
|
Attribute: nil,
|
|
})
|
|
}
|
|
|
|
regexInfo := loadCode(cfg, "regex:")
|
|
for _, info := range regexInfo {
|
|
if _, err := regexp.Compile(info.code); err != nil {
|
|
return nil, common.NewError("invalid regular expression: " + info.code).Base(err)
|
|
}
|
|
client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{
|
|
Type: v2router.Domain_Regex,
|
|
Value: info.code,
|
|
Attribute: nil,
|
|
})
|
|
}
|
|
|
|
// Just for compatibility with V2Ray rule type `regexp`
|
|
regexpInfo := loadCode(cfg, "regexp:")
|
|
for _, info := range regexpInfo {
|
|
if _, err := regexp.Compile(info.code); err != nil {
|
|
return nil, common.NewError("invalid regular expression: " + info.code).Base(err)
|
|
}
|
|
client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{
|
|
Type: v2router.Domain_Regex,
|
|
Value: info.code,
|
|
Attribute: nil,
|
|
})
|
|
}
|
|
|
|
fullInfo := loadCode(cfg, "full:")
|
|
for _, info := range fullInfo {
|
|
client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{
|
|
Type: v2router.Domain_Full,
|
|
Value: strings.ToLower(info.code),
|
|
Attribute: nil,
|
|
})
|
|
}
|
|
|
|
cidrInfo := loadCode(cfg, "cidr:")
|
|
for _, info := range cidrInfo {
|
|
tmp := strings.Split(info.code, "/")
|
|
if len(tmp) != 2 {
|
|
return nil, common.NewError("invalid cidr: " + info.code)
|
|
}
|
|
ip := net.ParseIP(tmp[0])
|
|
if ip == nil {
|
|
return nil, common.NewError("invalid cidr ip: " + info.code)
|
|
}
|
|
prefix, err := strconv.ParseInt(tmp[1], 10, 32)
|
|
if err != nil {
|
|
return nil, common.NewError("invalid prefix").Base(err)
|
|
}
|
|
client.cidrs[info.strategy] = append(client.cidrs[info.strategy], &v2router.CIDR{
|
|
Ip: ip,
|
|
Prefix: uint32(prefix),
|
|
})
|
|
}
|
|
|
|
log.Info("router client created")
|
|
|
|
runtime.ReadMemStats(&m4)
|
|
|
|
log.Debugf("GeoIP rules -> Alloc: %s; TotalAlloc: %s", common.HumanFriendlyTraffic(m2.Alloc-m1.Alloc), common.HumanFriendlyTraffic(m2.TotalAlloc-m1.TotalAlloc))
|
|
log.Debugf("GeoSite rules -> Alloc: %s; TotalAlloc: %s", common.HumanFriendlyTraffic(m3.Alloc-m2.Alloc), common.HumanFriendlyTraffic(m3.TotalAlloc-m2.TotalAlloc))
|
|
log.Debugf("Plaintext rules -> Alloc: %s; TotalAlloc: %s", common.HumanFriendlyTraffic(m4.Alloc-m3.Alloc), common.HumanFriendlyTraffic(m4.TotalAlloc-m3.TotalAlloc))
|
|
log.Debugf("Total(router) -> Alloc: %s; TotalAlloc: %s", common.HumanFriendlyTraffic(m4.Alloc-m1.Alloc), common.HumanFriendlyTraffic(m4.TotalAlloc-m1.TotalAlloc))
|
|
|
|
return client, nil
|
|
}
|