mirror of
https://github.com/XTLS/Xray-core.git
synced 2025-04-30 09:18:34 +00:00
v1.0.0
This commit is contained in:
parent
47d23e9972
commit
c7f7c08ead
711 changed files with 82154 additions and 2 deletions
41
infra/conf/api.go
Normal file
41
infra/conf/api.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/v1/app/commander"
|
||||
loggerservice "github.com/xtls/xray-core/v1/app/log/command"
|
||||
handlerservice "github.com/xtls/xray-core/v1/app/proxyman/command"
|
||||
statsservice "github.com/xtls/xray-core/v1/app/stats/command"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
)
|
||||
|
||||
type APIConfig struct {
|
||||
Tag string `json:"tag"`
|
||||
Services []string `json:"services"`
|
||||
}
|
||||
|
||||
func (c *APIConfig) Build() (*commander.Config, error) {
|
||||
if c.Tag == "" {
|
||||
return nil, newError("API tag can't be empty.")
|
||||
}
|
||||
|
||||
services := make([]*serial.TypedMessage, 0, 16)
|
||||
for _, s := range c.Services {
|
||||
switch strings.ToLower(s) {
|
||||
case "reflectionservice":
|
||||
services = append(services, serial.ToTypedMessage(&commander.ReflectionConfig{}))
|
||||
case "handlerservice":
|
||||
services = append(services, serial.ToTypedMessage(&handlerservice.Config{}))
|
||||
case "loggerservice":
|
||||
services = append(services, serial.ToTypedMessage(&loggerservice.Config{}))
|
||||
case "statsservice":
|
||||
services = append(services, serial.ToTypedMessage(&statsservice.Config{}))
|
||||
}
|
||||
}
|
||||
|
||||
return &commander.Config{
|
||||
Tag: c.Tag,
|
||||
Service: services,
|
||||
}, nil
|
||||
}
|
53
infra/conf/blackhole.go
Normal file
53
infra/conf/blackhole.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/blackhole"
|
||||
)
|
||||
|
||||
type NoneResponse struct{}
|
||||
|
||||
func (*NoneResponse) Build() (proto.Message, error) {
|
||||
return new(blackhole.NoneResponse), nil
|
||||
}
|
||||
|
||||
type HTTPResponse struct{}
|
||||
|
||||
func (*HTTPResponse) Build() (proto.Message, error) {
|
||||
return new(blackhole.HTTPResponse), nil
|
||||
}
|
||||
|
||||
type BlackholeConfig struct {
|
||||
Response json.RawMessage `json:"response"`
|
||||
}
|
||||
|
||||
func (v *BlackholeConfig) Build() (proto.Message, error) {
|
||||
config := new(blackhole.Config)
|
||||
if v.Response != nil {
|
||||
response, _, err := configLoader.Load(v.Response)
|
||||
if err != nil {
|
||||
return nil, newError("Config: Failed to parse Blackhole response config.").Base(err)
|
||||
}
|
||||
responseSettings, err := response.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Response = serial.ToTypedMessage(responseSettings)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
var (
|
||||
configLoader = NewJSONConfigLoader(
|
||||
ConfigCreatorCache{
|
||||
"none": func() interface{} { return new(NoneResponse) },
|
||||
"http": func() interface{} { return new(HTTPResponse) },
|
||||
},
|
||||
"type",
|
||||
"")
|
||||
)
|
34
infra/conf/blackhole_test.go
Normal file
34
infra/conf/blackhole_test.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/blackhole"
|
||||
)
|
||||
|
||||
func TestHTTPResponseJSON(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(BlackholeConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"response": {
|
||||
"type": "http"
|
||||
}
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &blackhole.Config{
|
||||
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}),
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &blackhole.Config{},
|
||||
},
|
||||
})
|
||||
}
|
7
infra/conf/buildable.go
Normal file
7
infra/conf/buildable.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package conf
|
||||
|
||||
import "github.com/golang/protobuf/proto"
|
||||
|
||||
type Buildable interface {
|
||||
Build() (proto.Message, error)
|
||||
}
|
241
infra/conf/common.go
Normal file
241
infra/conf/common.go
Normal file
|
@ -0,0 +1,241 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
)
|
||||
|
||||
type StringList []string
|
||||
|
||||
func NewStringList(raw []string) *StringList {
|
||||
list := StringList(raw)
|
||||
return &list
|
||||
}
|
||||
|
||||
func (v StringList) Len() int {
|
||||
return len(v)
|
||||
}
|
||||
|
||||
func (v *StringList) UnmarshalJSON(data []byte) error {
|
||||
var strarray []string
|
||||
if err := json.Unmarshal(data, &strarray); err == nil {
|
||||
*v = *NewStringList(strarray)
|
||||
return nil
|
||||
}
|
||||
|
||||
var rawstr string
|
||||
if err := json.Unmarshal(data, &rawstr); err == nil {
|
||||
strlist := strings.Split(rawstr, ",")
|
||||
*v = *NewStringList(strlist)
|
||||
return nil
|
||||
}
|
||||
return newError("unknown format of a string list: " + string(data))
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
net.Address
|
||||
}
|
||||
|
||||
func (v *Address) UnmarshalJSON(data []byte) error {
|
||||
var rawStr string
|
||||
if err := json.Unmarshal(data, &rawStr); err != nil {
|
||||
return newError("invalid address: ", string(data)).Base(err)
|
||||
}
|
||||
v.Address = net.ParseAddress(rawStr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Address) Build() *net.IPOrDomain {
|
||||
return net.NewIPOrDomain(v.Address)
|
||||
}
|
||||
|
||||
type Network string
|
||||
|
||||
func (v Network) Build() net.Network {
|
||||
switch strings.ToLower(string(v)) {
|
||||
case "tcp":
|
||||
return net.Network_TCP
|
||||
case "udp":
|
||||
return net.Network_UDP
|
||||
case "unix":
|
||||
return net.Network_UNIX
|
||||
default:
|
||||
return net.Network_Unknown
|
||||
}
|
||||
}
|
||||
|
||||
type NetworkList []Network
|
||||
|
||||
func (v *NetworkList) UnmarshalJSON(data []byte) error {
|
||||
var strarray []Network
|
||||
if err := json.Unmarshal(data, &strarray); err == nil {
|
||||
nl := NetworkList(strarray)
|
||||
*v = nl
|
||||
return nil
|
||||
}
|
||||
|
||||
var rawstr Network
|
||||
if err := json.Unmarshal(data, &rawstr); err == nil {
|
||||
strlist := strings.Split(string(rawstr), ",")
|
||||
nl := make([]Network, len(strlist))
|
||||
for idx, network := range strlist {
|
||||
nl[idx] = Network(network)
|
||||
}
|
||||
*v = nl
|
||||
return nil
|
||||
}
|
||||
return newError("unknown format of a string list: " + string(data))
|
||||
}
|
||||
|
||||
func (v *NetworkList) Build() []net.Network {
|
||||
if v == nil {
|
||||
return []net.Network{net.Network_TCP}
|
||||
}
|
||||
|
||||
list := make([]net.Network, 0, len(*v))
|
||||
for _, network := range *v {
|
||||
list = append(list, network.Build())
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func parseIntPort(data []byte) (net.Port, error) {
|
||||
var intPort uint32
|
||||
err := json.Unmarshal(data, &intPort)
|
||||
if err != nil {
|
||||
return net.Port(0), err
|
||||
}
|
||||
return net.PortFromInt(intPort)
|
||||
}
|
||||
|
||||
func parseStringPort(s string) (net.Port, net.Port, error) {
|
||||
if strings.HasPrefix(s, "env:") {
|
||||
s = s[4:]
|
||||
s = os.Getenv(s)
|
||||
}
|
||||
|
||||
pair := strings.SplitN(s, "-", 2)
|
||||
if len(pair) == 0 {
|
||||
return net.Port(0), net.Port(0), newError("invalid port range: ", s)
|
||||
}
|
||||
if len(pair) == 1 {
|
||||
port, err := net.PortFromString(pair[0])
|
||||
return port, port, err
|
||||
}
|
||||
|
||||
fromPort, err := net.PortFromString(pair[0])
|
||||
if err != nil {
|
||||
return net.Port(0), net.Port(0), err
|
||||
}
|
||||
toPort, err := net.PortFromString(pair[1])
|
||||
if err != nil {
|
||||
return net.Port(0), net.Port(0), err
|
||||
}
|
||||
return fromPort, toPort, nil
|
||||
}
|
||||
|
||||
func parseJSONStringPort(data []byte) (net.Port, net.Port, error) {
|
||||
var s string
|
||||
err := json.Unmarshal(data, &s)
|
||||
if err != nil {
|
||||
return net.Port(0), net.Port(0), err
|
||||
}
|
||||
return parseStringPort(s)
|
||||
}
|
||||
|
||||
type PortRange struct {
|
||||
From uint32
|
||||
To uint32
|
||||
}
|
||||
|
||||
func (v *PortRange) Build() *net.PortRange {
|
||||
return &net.PortRange{
|
||||
From: v.From,
|
||||
To: v.To,
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
|
||||
func (v *PortRange) UnmarshalJSON(data []byte) error {
|
||||
port, err := parseIntPort(data)
|
||||
if err == nil {
|
||||
v.From = uint32(port)
|
||||
v.To = uint32(port)
|
||||
return nil
|
||||
}
|
||||
|
||||
from, to, err := parseJSONStringPort(data)
|
||||
if err == nil {
|
||||
v.From = uint32(from)
|
||||
v.To = uint32(to)
|
||||
if v.From > v.To {
|
||||
return newError("invalid port range ", v.From, " -> ", v.To)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return newError("invalid port range: ", string(data))
|
||||
}
|
||||
|
||||
type PortList struct {
|
||||
Range []PortRange
|
||||
}
|
||||
|
||||
func (list *PortList) Build() *net.PortList {
|
||||
portList := new(net.PortList)
|
||||
for _, r := range list.Range {
|
||||
portList.Range = append(portList.Range, r.Build())
|
||||
}
|
||||
return portList
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
|
||||
func (list *PortList) UnmarshalJSON(data []byte) error {
|
||||
var listStr string
|
||||
var number uint32
|
||||
if err := json.Unmarshal(data, &listStr); err != nil {
|
||||
if err2 := json.Unmarshal(data, &number); err2 != nil {
|
||||
return newError("invalid port: ", string(data)).Base(err2)
|
||||
}
|
||||
}
|
||||
rangelist := strings.Split(listStr, ",")
|
||||
for _, rangeStr := range rangelist {
|
||||
trimmed := strings.TrimSpace(rangeStr)
|
||||
if len(trimmed) > 0 {
|
||||
if strings.Contains(trimmed, "-") {
|
||||
from, to, err := parseStringPort(trimmed)
|
||||
if err != nil {
|
||||
return newError("invalid port range: ", trimmed).Base(err)
|
||||
}
|
||||
list.Range = append(list.Range, PortRange{From: uint32(from), To: uint32(to)})
|
||||
} else {
|
||||
port, err := parseIntPort([]byte(trimmed))
|
||||
if err != nil {
|
||||
return newError("invalid port: ", trimmed).Base(err)
|
||||
}
|
||||
list.Range = append(list.Range, PortRange{From: uint32(port), To: uint32(port)})
|
||||
}
|
||||
}
|
||||
}
|
||||
if number != 0 {
|
||||
list.Range = append(list.Range, PortRange{From: number, To: number})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type User struct {
|
||||
EmailString string `json:"email"`
|
||||
LevelByte byte `json:"level"`
|
||||
}
|
||||
|
||||
func (v *User) Build() *protocol.User {
|
||||
return &protocol.User{
|
||||
Email: v.EmailString,
|
||||
Level: uint32(v.LevelByte),
|
||||
}
|
||||
}
|
231
infra/conf/common_test.go
Normal file
231
infra/conf/common_test.go
Normal file
|
@ -0,0 +1,231 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
)
|
||||
|
||||
func TestStringListUnmarshalError(t *testing.T) {
|
||||
rawJSON := `1234`
|
||||
list := new(StringList)
|
||||
err := json.Unmarshal([]byte(rawJSON), list)
|
||||
if err == nil {
|
||||
t.Error("expected error, but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringListLen(t *testing.T) {
|
||||
rawJSON := `"a, b, c, d"`
|
||||
var list StringList
|
||||
err := json.Unmarshal([]byte(rawJSON), &list)
|
||||
common.Must(err)
|
||||
if r := cmp.Diff([]string(list), []string{"a", " b", " c", " d"}); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPParsing(t *testing.T) {
|
||||
rawJSON := "\"8.8.8.8\""
|
||||
var address Address
|
||||
err := json.Unmarshal([]byte(rawJSON), &address)
|
||||
common.Must(err)
|
||||
if r := cmp.Diff(address.IP(), net.IP{8, 8, 8, 8}); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainParsing(t *testing.T) {
|
||||
rawJSON := "\"example.com\""
|
||||
var address Address
|
||||
common.Must(json.Unmarshal([]byte(rawJSON), &address))
|
||||
if address.Domain() != "example.com" {
|
||||
t.Error("domain: ", address.Domain())
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLParsing(t *testing.T) {
|
||||
{
|
||||
rawJSON := "\"https://dns.google/dns-query\""
|
||||
var address Address
|
||||
common.Must(json.Unmarshal([]byte(rawJSON), &address))
|
||||
if address.Domain() != "https://dns.google/dns-query" {
|
||||
t.Error("URL: ", address.Domain())
|
||||
}
|
||||
}
|
||||
{
|
||||
rawJSON := "\"https+local://dns.google/dns-query\""
|
||||
var address Address
|
||||
common.Must(json.Unmarshal([]byte(rawJSON), &address))
|
||||
if address.Domain() != "https+local://dns.google/dns-query" {
|
||||
t.Error("URL: ", address.Domain())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidAddressJson(t *testing.T) {
|
||||
rawJSON := "1234"
|
||||
var address Address
|
||||
err := json.Unmarshal([]byte(rawJSON), &address)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNetwork(t *testing.T) {
|
||||
var network Network
|
||||
common.Must(json.Unmarshal([]byte(`"tcp"`), &network))
|
||||
if v := network.Build(); v != net.Network_TCP {
|
||||
t.Error("network: ", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArrayNetworkList(t *testing.T) {
|
||||
var list NetworkList
|
||||
common.Must(json.Unmarshal([]byte("[\"Tcp\"]"), &list))
|
||||
|
||||
nlist := list.Build()
|
||||
if !net.HasNetwork(nlist, net.Network_TCP) {
|
||||
t.Error("no tcp network")
|
||||
}
|
||||
if net.HasNetwork(nlist, net.Network_UDP) {
|
||||
t.Error("has udp network")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNetworkList(t *testing.T) {
|
||||
var list NetworkList
|
||||
common.Must(json.Unmarshal([]byte("\"TCP, ip\""), &list))
|
||||
|
||||
nlist := list.Build()
|
||||
if !net.HasNetwork(nlist, net.Network_TCP) {
|
||||
t.Error("no tcp network")
|
||||
}
|
||||
if net.HasNetwork(nlist, net.Network_UDP) {
|
||||
t.Error("has udp network")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidNetworkJson(t *testing.T) {
|
||||
var list NetworkList
|
||||
err := json.Unmarshal([]byte("0"), &list)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntPort(t *testing.T) {
|
||||
var portRange PortRange
|
||||
common.Must(json.Unmarshal([]byte("1234"), &portRange))
|
||||
|
||||
if r := cmp.Diff(portRange, PortRange{
|
||||
From: 1234, To: 1234,
|
||||
}); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverRangeIntPort(t *testing.T) {
|
||||
var portRange PortRange
|
||||
err := json.Unmarshal([]byte("70000"), &portRange)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte("-1"), &portRange)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvPort(t *testing.T) {
|
||||
common.Must(os.Setenv("PORT", "1234"))
|
||||
|
||||
var portRange PortRange
|
||||
common.Must(json.Unmarshal([]byte("\"env:PORT\""), &portRange))
|
||||
|
||||
if r := cmp.Diff(portRange, PortRange{
|
||||
From: 1234, To: 1234,
|
||||
}); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleStringPort(t *testing.T) {
|
||||
var portRange PortRange
|
||||
common.Must(json.Unmarshal([]byte("\"1234\""), &portRange))
|
||||
|
||||
if r := cmp.Diff(portRange, PortRange{
|
||||
From: 1234, To: 1234,
|
||||
}); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringPairPort(t *testing.T) {
|
||||
var portRange PortRange
|
||||
common.Must(json.Unmarshal([]byte("\"1234-5678\""), &portRange))
|
||||
|
||||
if r := cmp.Diff(portRange, PortRange{
|
||||
From: 1234, To: 5678,
|
||||
}); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverRangeStringPort(t *testing.T) {
|
||||
var portRange PortRange
|
||||
err := json.Unmarshal([]byte("\"65536\""), &portRange)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte("\"70000-80000\""), &portRange)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte("\"1-90000\""), &portRange)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte("\"700-600\""), &portRange)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserParsing(t *testing.T) {
|
||||
user := new(User)
|
||||
common.Must(json.Unmarshal([]byte(`{
|
||||
"id": "96edb838-6d68-42ef-a933-25f7ac3a9d09",
|
||||
"email": "love@example.com",
|
||||
"level": 1,
|
||||
"alterId": 100
|
||||
}`), user))
|
||||
|
||||
nUser := user.Build()
|
||||
if r := cmp.Diff(nUser, &protocol.User{
|
||||
Level: 1,
|
||||
Email: "love@example.com",
|
||||
}, cmpopts.IgnoreUnexported(protocol.User{})); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidUserJson(t *testing.T) {
|
||||
user := new(User)
|
||||
err := json.Unmarshal([]byte(`{"email": 1234}`), user)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
}
|
3
infra/conf/conf.go
Normal file
3
infra/conf/conf.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package conf
|
||||
|
||||
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
|
240
infra/conf/dns.go
Normal file
240
infra/conf/dns.go
Normal file
|
@ -0,0 +1,240 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/v1/app/dns"
|
||||
"github.com/xtls/xray-core/v1/app/router"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
)
|
||||
|
||||
type NameServerConfig struct {
|
||||
Address *Address
|
||||
Port uint16
|
||||
Domains []string
|
||||
ExpectIPs StringList
|
||||
}
|
||||
|
||||
func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
|
||||
var address Address
|
||||
if err := json.Unmarshal(data, &address); err == nil {
|
||||
c.Address = &address
|
||||
return nil
|
||||
}
|
||||
|
||||
var advanced struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Domains []string `json:"domains"`
|
||||
ExpectIPs StringList `json:"expectIps"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &advanced); err == nil {
|
||||
c.Address = advanced.Address
|
||||
c.Port = advanced.Port
|
||||
c.Domains = advanced.Domains
|
||||
c.ExpectIPs = advanced.ExpectIPs
|
||||
return nil
|
||||
}
|
||||
|
||||
return newError("failed to parse name server: ", string(data))
|
||||
}
|
||||
|
||||
func toDomainMatchingType(t router.Domain_Type) dns.DomainMatchingType {
|
||||
switch t {
|
||||
case router.Domain_Domain:
|
||||
return dns.DomainMatchingType_Subdomain
|
||||
case router.Domain_Full:
|
||||
return dns.DomainMatchingType_Full
|
||||
case router.Domain_Plain:
|
||||
return dns.DomainMatchingType_Keyword
|
||||
case router.Domain_Regex:
|
||||
return dns.DomainMatchingType_Regex
|
||||
default:
|
||||
panic("unknown domain type")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *NameServerConfig) Build() (*dns.NameServer, error) {
|
||||
if c.Address == nil {
|
||||
return nil, newError("NameServer address is not specified.")
|
||||
}
|
||||
|
||||
var domains []*dns.NameServer_PriorityDomain
|
||||
var originalRules []*dns.NameServer_OriginalRule
|
||||
|
||||
for _, rule := range c.Domains {
|
||||
parsedDomain, err := parseDomainRule(rule)
|
||||
if err != nil {
|
||||
return nil, newError("invalid domain rule: ", rule).Base(err)
|
||||
}
|
||||
|
||||
for _, pd := range parsedDomain {
|
||||
domains = append(domains, &dns.NameServer_PriorityDomain{
|
||||
Type: toDomainMatchingType(pd.Type),
|
||||
Domain: pd.Value,
|
||||
})
|
||||
}
|
||||
originalRules = append(originalRules, &dns.NameServer_OriginalRule{
|
||||
Rule: rule,
|
||||
Size: uint32(len(parsedDomain)),
|
||||
})
|
||||
}
|
||||
|
||||
geoipList, err := toCidrList(c.ExpectIPs)
|
||||
if err != nil {
|
||||
return nil, newError("invalid ip rule: ", c.ExpectIPs).Base(err)
|
||||
}
|
||||
|
||||
return &dns.NameServer{
|
||||
Address: &net.Endpoint{
|
||||
Network: net.Network_UDP,
|
||||
Address: c.Address.Build(),
|
||||
Port: uint32(c.Port),
|
||||
},
|
||||
PrioritizedDomain: domains,
|
||||
Geoip: geoipList,
|
||||
OriginalRules: originalRules,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var typeMap = map[router.Domain_Type]dns.DomainMatchingType{
|
||||
router.Domain_Full: dns.DomainMatchingType_Full,
|
||||
router.Domain_Domain: dns.DomainMatchingType_Subdomain,
|
||||
router.Domain_Plain: dns.DomainMatchingType_Keyword,
|
||||
router.Domain_Regex: dns.DomainMatchingType_Regex,
|
||||
}
|
||||
|
||||
// DNSConfig is a JSON serializable object for dns.Config.
|
||||
type DNSConfig struct {
|
||||
Servers []*NameServerConfig `json:"servers"`
|
||||
Hosts map[string]*Address `json:"hosts"`
|
||||
ClientIP *Address `json:"clientIp"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
func getHostMapping(addr *Address) *dns.Config_HostMapping {
|
||||
if addr.Family().IsIP() {
|
||||
return &dns.Config_HostMapping{
|
||||
Ip: [][]byte{[]byte(addr.IP())},
|
||||
}
|
||||
} else {
|
||||
return &dns.Config_HostMapping{
|
||||
ProxiedDomain: addr.Domain(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *DNSConfig) Build() (*dns.Config, error) {
|
||||
config := &dns.Config{
|
||||
Tag: c.Tag,
|
||||
}
|
||||
|
||||
if c.ClientIP != nil {
|
||||
if !c.ClientIP.Family().IsIP() {
|
||||
return nil, newError("not an IP address:", c.ClientIP.String())
|
||||
}
|
||||
config.ClientIp = []byte(c.ClientIP.IP())
|
||||
}
|
||||
|
||||
for _, server := range c.Servers {
|
||||
ns, err := server.Build()
|
||||
if err != nil {
|
||||
return nil, newError("failed to build name server").Base(err)
|
||||
}
|
||||
config.NameServer = append(config.NameServer, ns)
|
||||
}
|
||||
|
||||
if c.Hosts != nil && len(c.Hosts) > 0 {
|
||||
domains := make([]string, 0, len(c.Hosts))
|
||||
for domain := range c.Hosts {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
sort.Strings(domains)
|
||||
|
||||
for _, domain := range domains {
|
||||
addr := c.Hosts[domain]
|
||||
var mappings []*dns.Config_HostMapping
|
||||
switch {
|
||||
case strings.HasPrefix(domain, "domain:"):
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Subdomain
|
||||
mapping.Domain = domain[7:]
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
case strings.HasPrefix(domain, "geosite:"):
|
||||
domains, err := loadGeositeWithAttr("geosite.dat", strings.ToUpper(domain[8:]))
|
||||
if err != nil {
|
||||
return nil, newError("invalid geosite settings: ", domain).Base(err)
|
||||
}
|
||||
for _, d := range domains {
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = typeMap[d.Type]
|
||||
mapping.Domain = d.Value
|
||||
mappings = append(mappings, mapping)
|
||||
}
|
||||
|
||||
case strings.HasPrefix(domain, "regexp:"):
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Regex
|
||||
mapping.Domain = domain[7:]
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
case strings.HasPrefix(domain, "keyword:"):
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Keyword
|
||||
mapping.Domain = domain[8:]
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
case strings.HasPrefix(domain, "full:"):
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Full
|
||||
mapping.Domain = domain[5:]
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
case strings.HasPrefix(domain, "dotless:"):
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Regex
|
||||
switch substr := domain[8:]; {
|
||||
case substr == "":
|
||||
mapping.Domain = "^[^.]*$"
|
||||
case !strings.Contains(substr, "."):
|
||||
mapping.Domain = "^[^.]*" + substr + "[^.]*$"
|
||||
default:
|
||||
return nil, newError("substr in dotless rule should not contain a dot: ", substr)
|
||||
}
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
case strings.HasPrefix(domain, "ext:"):
|
||||
kv := strings.Split(domain[4:], ":")
|
||||
if len(kv) != 2 {
|
||||
return nil, newError("invalid external resource: ", domain)
|
||||
}
|
||||
filename := kv[0]
|
||||
country := kv[1]
|
||||
domains, err := loadGeositeWithAttr(filename, country)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load domains: ", country, " from ", filename).Base(err)
|
||||
}
|
||||
for _, d := range domains {
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = typeMap[d.Type]
|
||||
mapping.Domain = d.Value
|
||||
mappings = append(mappings, mapping)
|
||||
}
|
||||
|
||||
default:
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Full
|
||||
mapping.Domain = domain
|
||||
mappings = append(mappings, mapping)
|
||||
}
|
||||
|
||||
config.StaticHosts = append(config.StaticHosts, mappings...)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
26
infra/conf/dns_proxy.go
Normal file
26
infra/conf/dns_proxy.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/proxy/dns"
|
||||
)
|
||||
|
||||
type DNSOutboundConfig struct {
|
||||
Network Network `json:"network"`
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
}
|
||||
|
||||
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
||||
config := &dns.Config{
|
||||
Server: &net.Endpoint{
|
||||
Network: c.Network.Build(),
|
||||
Port: uint32(c.Port),
|
||||
},
|
||||
}
|
||||
if c.Address != nil {
|
||||
config.Server.Address = c.Address.Build()
|
||||
}
|
||||
return config, nil
|
||||
}
|
33
infra/conf/dns_proxy_test.go
Normal file
33
infra/conf/dns_proxy_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/dns"
|
||||
)
|
||||
|
||||
func TestDnsProxyConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(DNSOutboundConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"address": "8.8.8.8",
|
||||
"port": 53,
|
||||
"network": "tcp"
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{
|
||||
Network: net.Network_TCP,
|
||||
Address: net.NewIPOrDomain(net.IPAddress([]byte{8, 8, 8, 8})),
|
||||
Port: 53,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
140
infra/conf/dns_test.go
Normal file
140
infra/conf/dns_test.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/xtls/xray-core/v1/app/dns"
|
||||
"github.com/xtls/xray-core/v1/app/router"
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/platform"
|
||||
"github.com/xtls/xray-core/v1/common/platform/filesystem"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
)
|
||||
|
||||
func init() {
|
||||
wd, err := os.Getwd()
|
||||
common.Must(err)
|
||||
|
||||
if _, err := os.Stat(platform.GetAssetLocation("geoip.dat")); err != nil && os.IsNotExist(err) {
|
||||
common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat")))
|
||||
}
|
||||
|
||||
geositeFilePath := filepath.Join(wd, "geosite.dat")
|
||||
os.Setenv("xray.location.asset", wd)
|
||||
geositeFile, err := os.OpenFile(geositeFilePath, os.O_CREATE|os.O_WRONLY, 0600)
|
||||
common.Must(err)
|
||||
defer geositeFile.Close()
|
||||
|
||||
list := &router.GeoSiteList{
|
||||
Entry: []*router.GeoSite{
|
||||
{
|
||||
CountryCode: "TEST",
|
||||
Domain: []*router.Domain{
|
||||
{Type: router.Domain_Full, Value: "example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
listBytes, err := proto.Marshal(list)
|
||||
common.Must(err)
|
||||
common.Must2(geositeFile.Write(listBytes))
|
||||
}
|
||||
func TestDNSConfigParsing(t *testing.T) {
|
||||
geositePath := platform.GetAssetLocation("geosite.dat")
|
||||
defer func() {
|
||||
os.Remove(geositePath)
|
||||
os.Unsetenv("xray.location.asset")
|
||||
}()
|
||||
|
||||
parserCreator := func() func(string) (proto.Message, error) {
|
||||
return func(s string) (proto.Message, error) {
|
||||
config := new(DNSConfig)
|
||||
if err := json.Unmarshal([]byte(s), config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config.Build()
|
||||
}
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"servers": [{
|
||||
"address": "8.8.8.8",
|
||||
"port": 5353,
|
||||
"domains": ["domain:example.com"]
|
||||
}],
|
||||
"hosts": {
|
||||
"example.com": "127.0.0.1",
|
||||
"domain:example.com": "google.com",
|
||||
"geosite:test": "10.0.0.1",
|
||||
"keyword:google": "8.8.8.8",
|
||||
"regexp:.*\\.com": "8.8.4.4"
|
||||
},
|
||||
"clientIp": "10.0.0.1"
|
||||
}`,
|
||||
Parser: parserCreator(),
|
||||
Output: &dns.Config{
|
||||
NameServer: []*dns.NameServer{
|
||||
{
|
||||
Address: &net.Endpoint{
|
||||
Address: &net.IPOrDomain{
|
||||
Address: &net.IPOrDomain_Ip{
|
||||
Ip: []byte{8, 8, 8, 8},
|
||||
},
|
||||
},
|
||||
Network: net.Network_UDP,
|
||||
Port: 5353,
|
||||
},
|
||||
PrioritizedDomain: []*dns.NameServer_PriorityDomain{
|
||||
{
|
||||
Type: dns.DomainMatchingType_Subdomain,
|
||||
Domain: "example.com",
|
||||
},
|
||||
},
|
||||
OriginalRules: []*dns.NameServer_OriginalRule{
|
||||
{
|
||||
Rule: "domain:example.com",
|
||||
Size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
StaticHosts: []*dns.Config_HostMapping{
|
||||
{
|
||||
Type: dns.DomainMatchingType_Subdomain,
|
||||
Domain: "example.com",
|
||||
ProxiedDomain: "google.com",
|
||||
},
|
||||
{
|
||||
Type: dns.DomainMatchingType_Full,
|
||||
Domain: "example.com",
|
||||
Ip: [][]byte{{10, 0, 0, 1}},
|
||||
},
|
||||
{
|
||||
Type: dns.DomainMatchingType_Keyword,
|
||||
Domain: "google",
|
||||
Ip: [][]byte{{8, 8, 8, 8}},
|
||||
},
|
||||
{
|
||||
Type: dns.DomainMatchingType_Regex,
|
||||
Domain: ".*\\.com",
|
||||
Ip: [][]byte{{8, 8, 4, 4}},
|
||||
},
|
||||
{
|
||||
Type: dns.DomainMatchingType_Full,
|
||||
Domain: "example.com",
|
||||
Ip: [][]byte{{127, 0, 0, 1}},
|
||||
},
|
||||
},
|
||||
ClientIp: []byte{10, 0, 0, 1},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
28
infra/conf/dokodemo.go
Normal file
28
infra/conf/dokodemo.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/xtls/xray-core/v1/proxy/dokodemo"
|
||||
)
|
||||
|
||||
type DokodemoConfig struct {
|
||||
Host *Address `json:"address"`
|
||||
PortValue uint16 `json:"port"`
|
||||
NetworkList *NetworkList `json:"network"`
|
||||
TimeoutValue uint32 `json:"timeout"`
|
||||
Redirect bool `json:"followRedirect"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
}
|
||||
|
||||
func (v *DokodemoConfig) Build() (proto.Message, error) {
|
||||
config := new(dokodemo.Config)
|
||||
if v.Host != nil {
|
||||
config.Address = v.Host.Build()
|
||||
}
|
||||
config.Port = uint32(v.PortValue)
|
||||
config.Networks = v.NetworkList.Build()
|
||||
config.Timeout = v.TimeoutValue
|
||||
config.FollowRedirect = v.Redirect
|
||||
config.UserLevel = v.UserLevel
|
||||
return config, nil
|
||||
}
|
41
infra/conf/dokodemo_test.go
Normal file
41
infra/conf/dokodemo_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/dokodemo"
|
||||
)
|
||||
|
||||
func TestDokodemoConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(DokodemoConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"address": "8.8.8.8",
|
||||
"port": 53,
|
||||
"network": "tcp",
|
||||
"timeout": 10,
|
||||
"followRedirect": true,
|
||||
"userLevel": 1
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dokodemo.Config{
|
||||
Address: &net.IPOrDomain{
|
||||
Address: &net.IPOrDomain_Ip{
|
||||
Ip: []byte{8, 8, 8, 8},
|
||||
},
|
||||
},
|
||||
Port: 53,
|
||||
Networks: []net.Network{net.Network_TCP},
|
||||
Timeout: 10,
|
||||
FollowRedirect: true,
|
||||
UserLevel: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
9
infra/conf/errors.generated.go
Normal file
9
infra/conf/errors.generated.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package conf
|
||||
|
||||
import "github.com/xtls/xray-core/v1/common/errors"
|
||||
|
||||
type errPathObjHolder struct{}
|
||||
|
||||
func newError(values ...interface{}) *errors.Error {
|
||||
return errors.New(values...).WithPathObj(errPathObjHolder{})
|
||||
}
|
57
infra/conf/freedom.go
Normal file
57
infra/conf/freedom.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
v2net "github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/proxy/freedom"
|
||||
)
|
||||
|
||||
type FreedomConfig struct {
|
||||
DomainStrategy string `json:"domainStrategy"`
|
||||
Timeout *uint32 `json:"timeout"`
|
||||
Redirect string `json:"redirect"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *FreedomConfig) Build() (proto.Message, error) {
|
||||
config := new(freedom.Config)
|
||||
config.DomainStrategy = freedom.Config_AS_IS
|
||||
switch strings.ToLower(c.DomainStrategy) {
|
||||
case "useip", "use_ip":
|
||||
config.DomainStrategy = freedom.Config_USE_IP
|
||||
case "useip4", "useipv4", "use_ipv4", "use_ip_v4", "use_ip4":
|
||||
config.DomainStrategy = freedom.Config_USE_IP4
|
||||
case "useip6", "useipv6", "use_ipv6", "use_ip_v6", "use_ip6":
|
||||
config.DomainStrategy = freedom.Config_USE_IP6
|
||||
}
|
||||
|
||||
if c.Timeout != nil {
|
||||
config.Timeout = *c.Timeout
|
||||
}
|
||||
config.UserLevel = c.UserLevel
|
||||
if len(c.Redirect) > 0 {
|
||||
host, portStr, err := net.SplitHostPort(c.Redirect)
|
||||
if err != nil {
|
||||
return nil, newError("invalid redirect address: ", c.Redirect, ": ", err).Base(err)
|
||||
}
|
||||
port, err := v2net.PortFromString(portStr)
|
||||
if err != nil {
|
||||
return nil, newError("invalid redirect port: ", c.Redirect, ": ", err).Base(err)
|
||||
}
|
||||
config.DestinationOverride = &freedom.DestinationOverride{
|
||||
Server: &protocol.ServerEndpoint{
|
||||
Port: uint32(port),
|
||||
},
|
||||
}
|
||||
|
||||
if len(host) > 0 {
|
||||
config.DestinationOverride.Server.Address = v2net.NewIPOrDomain(v2net.ParseAddress(host))
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
43
infra/conf/freedom_test.go
Normal file
43
infra/conf/freedom_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/freedom"
|
||||
)
|
||||
|
||||
func TestFreedomConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(FreedomConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"domainStrategy": "AsIs",
|
||||
"timeout": 10,
|
||||
"redirect": "127.0.0.1:3366",
|
||||
"userLevel": 1
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &freedom.Config{
|
||||
DomainStrategy: freedom.Config_AS_IS,
|
||||
Timeout: 10,
|
||||
DestinationOverride: &freedom.DestinationOverride{
|
||||
Server: &protocol.ServerEndpoint{
|
||||
Address: &net.IPOrDomain{
|
||||
Address: &net.IPOrDomain_Ip{
|
||||
Ip: []byte{127, 0, 0, 1},
|
||||
},
|
||||
},
|
||||
Port: 3366,
|
||||
},
|
||||
},
|
||||
UserLevel: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
36
infra/conf/general_test.go
Normal file
36
infra/conf/general_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
)
|
||||
|
||||
func loadJSON(creator func() Buildable) func(string) (proto.Message, error) {
|
||||
return func(s string) (proto.Message, error) {
|
||||
instance := creator()
|
||||
if err := json.Unmarshal([]byte(s), instance); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return instance.Build()
|
||||
}
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
Input string
|
||||
Parser func(string) (proto.Message, error)
|
||||
Output proto.Message
|
||||
}
|
||||
|
||||
func runMultiTestCase(t *testing.T, testCases []TestCase) {
|
||||
for _, testCase := range testCases {
|
||||
actual, err := testCase.Parser(testCase.Input)
|
||||
common.Must(err)
|
||||
if !proto.Equal(actual, testCase.Output) {
|
||||
t.Fatalf("Failed in test case:\n%s\nActual:\n%v\nExpected:\n%v", testCase.Input, actual, testCase.Output)
|
||||
}
|
||||
}
|
||||
}
|
80
infra/conf/http.go
Normal file
80
infra/conf/http.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/http"
|
||||
)
|
||||
|
||||
type HTTPAccount struct {
|
||||
Username string `json:"user"`
|
||||
Password string `json:"pass"`
|
||||
}
|
||||
|
||||
func (v *HTTPAccount) Build() *http.Account {
|
||||
return &http.Account{
|
||||
Username: v.Username,
|
||||
Password: v.Password,
|
||||
}
|
||||
}
|
||||
|
||||
type HTTPServerConfig struct {
|
||||
Timeout uint32 `json:"timeout"`
|
||||
Accounts []*HTTPAccount `json:"accounts"`
|
||||
Transparent bool `json:"allowTransparent"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
}
|
||||
|
||||
func (c *HTTPServerConfig) Build() (proto.Message, error) {
|
||||
config := &http.ServerConfig{
|
||||
Timeout: c.Timeout,
|
||||
AllowTransparent: c.Transparent,
|
||||
UserLevel: c.UserLevel,
|
||||
}
|
||||
|
||||
if len(c.Accounts) > 0 {
|
||||
config.Accounts = make(map[string]string)
|
||||
for _, account := range c.Accounts {
|
||||
config.Accounts[account.Username] = account.Password
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type HTTPRemoteConfig struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Users []json.RawMessage `json:"users"`
|
||||
}
|
||||
type HTTPClientConfig struct {
|
||||
Servers []*HTTPRemoteConfig `json:"servers"`
|
||||
}
|
||||
|
||||
func (v *HTTPClientConfig) Build() (proto.Message, error) {
|
||||
config := new(http.ClientConfig)
|
||||
config.Server = make([]*protocol.ServerEndpoint, len(v.Servers))
|
||||
for idx, serverConfig := range v.Servers {
|
||||
server := &protocol.ServerEndpoint{
|
||||
Address: serverConfig.Address.Build(),
|
||||
Port: uint32(serverConfig.Port),
|
||||
}
|
||||
for _, rawUser := range serverConfig.Users {
|
||||
user := new(protocol.User)
|
||||
if err := json.Unmarshal(rawUser, user); err != nil {
|
||||
return nil, newError("failed to parse HTTP user").Base(err).AtError()
|
||||
}
|
||||
account := new(HTTPAccount)
|
||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||
return nil, newError("failed to parse HTTP account").Base(err).AtError()
|
||||
}
|
||||
user.Account = serial.ToTypedMessage(account.Build())
|
||||
server.User = append(server.User, user)
|
||||
}
|
||||
config.Server[idx] = server
|
||||
}
|
||||
return config, nil
|
||||
}
|
39
infra/conf/http_test.go
Normal file
39
infra/conf/http_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/http"
|
||||
)
|
||||
|
||||
func TestHTTPServerConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(HTTPServerConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"timeout": 10,
|
||||
"accounts": [
|
||||
{
|
||||
"user": "my-username",
|
||||
"pass": "my-password"
|
||||
}
|
||||
],
|
||||
"allowTransparent": true,
|
||||
"userLevel": 1
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &http.ServerConfig{
|
||||
Accounts: map[string]string{
|
||||
"my-username": "my-password",
|
||||
},
|
||||
AllowTransparent: true,
|
||||
UserLevel: 1,
|
||||
Timeout: 10,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
133
infra/conf/json/reader.go
Normal file
133
infra/conf/json/reader.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
)
|
||||
|
||||
// State is the internal state of parser.
|
||||
type State byte
|
||||
|
||||
const (
|
||||
StateContent State = iota
|
||||
StateEscape
|
||||
StateDoubleQuote
|
||||
StateDoubleQuoteEscape
|
||||
StateSingleQuote
|
||||
StateSingleQuoteEscape
|
||||
StateComment
|
||||
StateSlash
|
||||
StateMultilineComment
|
||||
StateMultilineCommentStar
|
||||
)
|
||||
|
||||
// Reader is a reader for filtering comments.
|
||||
// It supports Java style single and multi line comment syntax, and Python style single line comment syntax.
|
||||
type Reader struct {
|
||||
io.Reader
|
||||
|
||||
state State
|
||||
br *buf.BufferedReader
|
||||
}
|
||||
|
||||
// Read implements io.Reader.Read(). Buffer must be at least 3 bytes.
|
||||
func (v *Reader) Read(b []byte) (int, error) {
|
||||
if v.br == nil {
|
||||
v.br = &buf.BufferedReader{Reader: buf.NewReader(v.Reader)}
|
||||
}
|
||||
|
||||
p := b[:0]
|
||||
for len(p) < len(b)-2 {
|
||||
x, err := v.br.ReadByte()
|
||||
if err != nil {
|
||||
if len(p) == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
switch v.state {
|
||||
case StateContent:
|
||||
switch x {
|
||||
case '"':
|
||||
v.state = StateDoubleQuote
|
||||
p = append(p, x)
|
||||
case '\'':
|
||||
v.state = StateSingleQuote
|
||||
p = append(p, x)
|
||||
case '\\':
|
||||
v.state = StateEscape
|
||||
case '#':
|
||||
v.state = StateComment
|
||||
case '/':
|
||||
v.state = StateSlash
|
||||
default:
|
||||
p = append(p, x)
|
||||
}
|
||||
case StateEscape:
|
||||
p = append(p, '\\', x)
|
||||
v.state = StateContent
|
||||
case StateDoubleQuote:
|
||||
switch x {
|
||||
case '"':
|
||||
v.state = StateContent
|
||||
p = append(p, x)
|
||||
case '\\':
|
||||
v.state = StateDoubleQuoteEscape
|
||||
default:
|
||||
p = append(p, x)
|
||||
}
|
||||
case StateDoubleQuoteEscape:
|
||||
p = append(p, '\\', x)
|
||||
v.state = StateDoubleQuote
|
||||
case StateSingleQuote:
|
||||
switch x {
|
||||
case '\'':
|
||||
v.state = StateContent
|
||||
p = append(p, x)
|
||||
case '\\':
|
||||
v.state = StateSingleQuoteEscape
|
||||
default:
|
||||
p = append(p, x)
|
||||
}
|
||||
case StateSingleQuoteEscape:
|
||||
p = append(p, '\\', x)
|
||||
v.state = StateSingleQuote
|
||||
case StateComment:
|
||||
if x == '\n' {
|
||||
v.state = StateContent
|
||||
p = append(p, '\n')
|
||||
}
|
||||
case StateSlash:
|
||||
switch x {
|
||||
case '/':
|
||||
v.state = StateComment
|
||||
case '*':
|
||||
v.state = StateMultilineComment
|
||||
default:
|
||||
p = append(p, '/', x)
|
||||
}
|
||||
case StateMultilineComment:
|
||||
switch x {
|
||||
case '*':
|
||||
v.state = StateMultilineCommentStar
|
||||
case '\n':
|
||||
p = append(p, '\n')
|
||||
}
|
||||
case StateMultilineCommentStar:
|
||||
switch x {
|
||||
case '/':
|
||||
v.state = StateContent
|
||||
case '*':
|
||||
// Stay
|
||||
case '\n':
|
||||
p = append(p, '\n')
|
||||
default:
|
||||
v.state = StateMultilineComment
|
||||
}
|
||||
default:
|
||||
panic("Unknown state.")
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
96
infra/conf/json/reader_test.go
Normal file
96
infra/conf/json/reader_test.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package json_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf/json"
|
||||
)
|
||||
|
||||
func TestReader(t *testing.T) {
|
||||
data := []struct {
|
||||
input string
|
||||
output string
|
||||
}{
|
||||
{
|
||||
`
|
||||
content #comment 1
|
||||
#comment 2
|
||||
content 2`,
|
||||
`
|
||||
content
|
||||
|
||||
content 2`},
|
||||
{`content`, `content`},
|
||||
{" ", " "},
|
||||
{`con/*abcd*/tent`, "content"},
|
||||
{`
|
||||
text // adlkhdf /*
|
||||
//comment adfkj
|
||||
text 2*/`, `
|
||||
text
|
||||
|
||||
text 2*`},
|
||||
{`"//"content`, `"//"content`},
|
||||
{`abcd'//'abcd`, `abcd'//'abcd`},
|
||||
{`"\""`, `"\""`},
|
||||
{`\"/*abcd*/\"`, `\"\"`},
|
||||
}
|
||||
|
||||
for _, testCase := range data {
|
||||
reader := &Reader{
|
||||
Reader: bytes.NewReader([]byte(testCase.input)),
|
||||
}
|
||||
|
||||
actual := make([]byte, 1024)
|
||||
n, err := reader.Read(actual)
|
||||
common.Must(err)
|
||||
if r := cmp.Diff(string(actual[:n]), testCase.output); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReader1(t *testing.T) {
|
||||
type dataStruct struct {
|
||||
input string
|
||||
output string
|
||||
}
|
||||
|
||||
bufLen := 8
|
||||
|
||||
data := []dataStruct{
|
||||
{"loooooooooooooooooooooooooooooooooooooooog", "loooooooooooooooooooooooooooooooooooooooog"},
|
||||
{`{"t": "\/testlooooooooooooooooooooooooooooong"}`, `{"t": "\/testlooooooooooooooooooooooooooooong"}`},
|
||||
{`{"t": "\/test"}`, `{"t": "\/test"}`},
|
||||
{`"\// fake comment"`, `"\// fake comment"`},
|
||||
{`"\/\/\/\/\/"`, `"\/\/\/\/\/"`},
|
||||
}
|
||||
|
||||
for _, testCase := range data {
|
||||
reader := &Reader{
|
||||
Reader: bytes.NewReader([]byte(testCase.input)),
|
||||
}
|
||||
target := make([]byte, 0)
|
||||
buf := make([]byte, bufLen)
|
||||
var n int
|
||||
var err error
|
||||
for n, err = reader.Read(buf); err == nil; n, err = reader.Read(buf) {
|
||||
if n > len(buf) {
|
||||
t.Error("n: ", n)
|
||||
}
|
||||
target = append(target, buf[:n]...)
|
||||
buf = make([]byte, bufLen)
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
t.Error("error: ", err)
|
||||
}
|
||||
if string(target) != testCase.output {
|
||||
t.Error("got ", string(target), " want ", testCase.output)
|
||||
}
|
||||
}
|
||||
}
|
83
infra/conf/loader.go
Normal file
83
infra/conf/loader.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ConfigCreator func() interface{}
|
||||
|
||||
type ConfigCreatorCache map[string]ConfigCreator
|
||||
|
||||
func (v ConfigCreatorCache) RegisterCreator(id string, creator ConfigCreator) error {
|
||||
if _, found := v[id]; found {
|
||||
return newError(id, " already registered.").AtError()
|
||||
}
|
||||
|
||||
v[id] = creator
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v ConfigCreatorCache) CreateConfig(id string) (interface{}, error) {
|
||||
creator, found := v[id]
|
||||
if !found {
|
||||
return nil, newError("unknown config id: ", id)
|
||||
}
|
||||
return creator(), nil
|
||||
}
|
||||
|
||||
type JSONConfigLoader struct {
|
||||
cache ConfigCreatorCache
|
||||
idKey string
|
||||
configKey string
|
||||
}
|
||||
|
||||
func NewJSONConfigLoader(cache ConfigCreatorCache, idKey string, configKey string) *JSONConfigLoader {
|
||||
return &JSONConfigLoader{
|
||||
idKey: idKey,
|
||||
configKey: configKey,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *JSONConfigLoader) LoadWithID(raw []byte, id string) (interface{}, error) {
|
||||
id = strings.ToLower(id)
|
||||
config, err := v.cache.CreateConfig(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(raw, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (v *JSONConfigLoader) Load(raw []byte) (interface{}, string, error) {
|
||||
var obj map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
rawID, found := obj[v.idKey]
|
||||
if !found {
|
||||
return nil, "", newError(v.idKey, " not found in JSON context").AtError()
|
||||
}
|
||||
var id string
|
||||
if err := json.Unmarshal(rawID, &id); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
rawConfig := json.RawMessage(raw)
|
||||
if len(v.configKey) > 0 {
|
||||
configValue, found := obj[v.configKey]
|
||||
if found {
|
||||
rawConfig = configValue
|
||||
} else {
|
||||
// Default to empty json object.
|
||||
rawConfig = json.RawMessage([]byte("{}"))
|
||||
}
|
||||
}
|
||||
config, err := v.LoadWithID([]byte(rawConfig), id)
|
||||
if err != nil {
|
||||
return nil, id, err
|
||||
}
|
||||
return config, id, nil
|
||||
}
|
61
infra/conf/log.go
Normal file
61
infra/conf/log.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/v1/app/log"
|
||||
clog "github.com/xtls/xray-core/v1/common/log"
|
||||
)
|
||||
|
||||
func DefaultLogConfig() *log.Config {
|
||||
return &log.Config{
|
||||
AccessLogType: log.LogType_None,
|
||||
ErrorLogType: log.LogType_Console,
|
||||
ErrorLogLevel: clog.Severity_Warning,
|
||||
}
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
AccessLog string `json:"access"`
|
||||
ErrorLog string `json:"error"`
|
||||
LogLevel string `json:"loglevel"`
|
||||
}
|
||||
|
||||
func (v *LogConfig) Build() *log.Config {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
config := &log.Config{
|
||||
ErrorLogType: log.LogType_Console,
|
||||
AccessLogType: log.LogType_Console,
|
||||
}
|
||||
|
||||
if v.AccessLog == "none" {
|
||||
config.AccessLogType = log.LogType_None
|
||||
} else if len(v.AccessLog) > 0 {
|
||||
config.AccessLogPath = v.AccessLog
|
||||
config.AccessLogType = log.LogType_File
|
||||
}
|
||||
if v.ErrorLog == "none" {
|
||||
config.ErrorLogType = log.LogType_None
|
||||
} else if len(v.ErrorLog) > 0 {
|
||||
config.ErrorLogPath = v.ErrorLog
|
||||
config.ErrorLogType = log.LogType_File
|
||||
}
|
||||
|
||||
level := strings.ToLower(v.LogLevel)
|
||||
switch level {
|
||||
case "debug":
|
||||
config.ErrorLogLevel = clog.Severity_Debug
|
||||
case "info":
|
||||
config.ErrorLogLevel = clog.Severity_Info
|
||||
case "error":
|
||||
config.ErrorLogLevel = clog.Severity_Error
|
||||
case "none":
|
||||
config.ErrorLogType = log.LogType_None
|
||||
config.AccessLogType = log.LogType_None
|
||||
default:
|
||||
config.ErrorLogLevel = clog.Severity_Warning
|
||||
}
|
||||
return config
|
||||
}
|
69
infra/conf/mtproto.go
Normal file
69
infra/conf/mtproto.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/mtproto"
|
||||
)
|
||||
|
||||
type MTProtoAccount struct {
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (a *MTProtoAccount) Build() (*mtproto.Account, error) {
|
||||
if len(a.Secret) != 32 {
|
||||
return nil, newError("MTProto secret must have 32 chars")
|
||||
}
|
||||
secret, err := hex.DecodeString(a.Secret)
|
||||
if err != nil {
|
||||
return nil, newError("failed to decode secret: ", a.Secret).Base(err)
|
||||
}
|
||||
return &mtproto.Account{
|
||||
Secret: secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MTProtoServerConfig struct {
|
||||
Users []json.RawMessage `json:"users"`
|
||||
}
|
||||
|
||||
func (c *MTProtoServerConfig) Build() (proto.Message, error) {
|
||||
config := &mtproto.ServerConfig{}
|
||||
|
||||
if len(c.Users) == 0 {
|
||||
return nil, newError("zero MTProto users configured.")
|
||||
}
|
||||
config.User = make([]*protocol.User, len(c.Users))
|
||||
for idx, rawData := range c.Users {
|
||||
user := new(protocol.User)
|
||||
if err := json.Unmarshal(rawData, user); err != nil {
|
||||
return nil, newError("invalid MTProto user").Base(err)
|
||||
}
|
||||
account := new(MTProtoAccount)
|
||||
if err := json.Unmarshal(rawData, account); err != nil {
|
||||
return nil, newError("invalid MTProto user").Base(err)
|
||||
}
|
||||
accountProto, err := account.Build()
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse MTProto user").Base(err)
|
||||
}
|
||||
user.Account = serial.ToTypedMessage(accountProto)
|
||||
config.User[idx] = user
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type MTProtoClientConfig struct {
|
||||
}
|
||||
|
||||
func (c *MTProtoClientConfig) Build() (proto.Message, error) {
|
||||
config := new(mtproto.ClientConfig)
|
||||
return config, nil
|
||||
}
|
40
infra/conf/mtproto_test.go
Normal file
40
infra/conf/mtproto_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/mtproto"
|
||||
)
|
||||
|
||||
func TestMTProtoServerConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(MTProtoServerConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"users": [{
|
||||
"email": "love@example.com",
|
||||
"level": 1,
|
||||
"secret": "b0cbcef5a486d9636472ac27f8e11a9d"
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &mtproto.ServerConfig{
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Email: "love@example.com",
|
||||
Level: 1,
|
||||
Account: serial.ToTypedMessage(&mtproto.Account{
|
||||
Secret: []byte{176, 203, 206, 245, 164, 134, 217, 99, 100, 114, 172, 39, 248, 225, 26, 157},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
100
infra/conf/policy.go
Normal file
100
infra/conf/policy.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/v1/app/policy"
|
||||
)
|
||||
|
||||
type Policy struct {
|
||||
Handshake *uint32 `json:"handshake"`
|
||||
ConnectionIdle *uint32 `json:"connIdle"`
|
||||
UplinkOnly *uint32 `json:"uplinkOnly"`
|
||||
DownlinkOnly *uint32 `json:"downlinkOnly"`
|
||||
StatsUserUplink bool `json:"statsUserUplink"`
|
||||
StatsUserDownlink bool `json:"statsUserDownlink"`
|
||||
BufferSize *int32 `json:"bufferSize"`
|
||||
}
|
||||
|
||||
func (t *Policy) Build() (*policy.Policy, error) {
|
||||
config := new(policy.Policy_Timeout)
|
||||
if t.Handshake != nil {
|
||||
config.Handshake = &policy.Second{Value: *t.Handshake}
|
||||
}
|
||||
if t.ConnectionIdle != nil {
|
||||
config.ConnectionIdle = &policy.Second{Value: *t.ConnectionIdle}
|
||||
}
|
||||
if t.UplinkOnly != nil {
|
||||
config.UplinkOnly = &policy.Second{Value: *t.UplinkOnly}
|
||||
}
|
||||
if t.DownlinkOnly != nil {
|
||||
config.DownlinkOnly = &policy.Second{Value: *t.DownlinkOnly}
|
||||
}
|
||||
|
||||
p := &policy.Policy{
|
||||
Timeout: config,
|
||||
Stats: &policy.Policy_Stats{
|
||||
UserUplink: t.StatsUserUplink,
|
||||
UserDownlink: t.StatsUserDownlink,
|
||||
},
|
||||
}
|
||||
|
||||
if t.BufferSize != nil {
|
||||
bs := int32(-1)
|
||||
if *t.BufferSize >= 0 {
|
||||
bs = (*t.BufferSize) * 1024
|
||||
}
|
||||
p.Buffer = &policy.Policy_Buffer{
|
||||
Connection: bs,
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
type SystemPolicy struct {
|
||||
StatsInboundUplink bool `json:"statsInboundUplink"`
|
||||
StatsInboundDownlink bool `json:"statsInboundDownlink"`
|
||||
StatsOutboundUplink bool `json:"statsOutboundUplink"`
|
||||
StatsOutboundDownlink bool `json:"statsOutboundDownlink"`
|
||||
}
|
||||
|
||||
func (p *SystemPolicy) Build() (*policy.SystemPolicy, error) {
|
||||
return &policy.SystemPolicy{
|
||||
Stats: &policy.SystemPolicy_Stats{
|
||||
InboundUplink: p.StatsInboundUplink,
|
||||
InboundDownlink: p.StatsInboundDownlink,
|
||||
OutboundUplink: p.StatsOutboundUplink,
|
||||
OutboundDownlink: p.StatsOutboundDownlink,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type PolicyConfig struct {
|
||||
Levels map[uint32]*Policy `json:"levels"`
|
||||
System *SystemPolicy `json:"system"`
|
||||
}
|
||||
|
||||
func (c *PolicyConfig) Build() (*policy.Config, error) {
|
||||
levels := make(map[uint32]*policy.Policy)
|
||||
for l, p := range c.Levels {
|
||||
if p != nil {
|
||||
pp, err := p.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
levels[l] = pp
|
||||
}
|
||||
}
|
||||
config := &policy.Config{
|
||||
Level: levels,
|
||||
}
|
||||
|
||||
if c.System != nil {
|
||||
sc, err := c.System.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.System = sc
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
40
infra/conf/policy_test.go
Normal file
40
infra/conf/policy_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
)
|
||||
|
||||
func TestBufferSize(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input int32
|
||||
Output int32
|
||||
}{
|
||||
{
|
||||
Input: 0,
|
||||
Output: 0,
|
||||
},
|
||||
{
|
||||
Input: -1,
|
||||
Output: -1,
|
||||
},
|
||||
{
|
||||
Input: 1,
|
||||
Output: 1024,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
bs := c.Input
|
||||
pConf := Policy{
|
||||
BufferSize: &bs,
|
||||
}
|
||||
p, err := pConf.Build()
|
||||
common.Must(err)
|
||||
if p.Buffer.Connection != c.Output {
|
||||
t.Error("expected buffer size ", c.Output, " but got ", p.Buffer.Connection)
|
||||
}
|
||||
}
|
||||
}
|
56
infra/conf/reverse.go
Normal file
56
infra/conf/reverse.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/xtls/xray-core/v1/app/reverse"
|
||||
)
|
||||
|
||||
type BridgeConfig struct {
|
||||
Tag string `json:"tag"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
func (c *BridgeConfig) Build() (*reverse.BridgeConfig, error) {
|
||||
return &reverse.BridgeConfig{
|
||||
Tag: c.Tag,
|
||||
Domain: c.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type PortalConfig struct {
|
||||
Tag string `json:"tag"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
func (c *PortalConfig) Build() (*reverse.PortalConfig, error) {
|
||||
return &reverse.PortalConfig{
|
||||
Tag: c.Tag,
|
||||
Domain: c.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ReverseConfig struct {
|
||||
Bridges []BridgeConfig `json:"bridges"`
|
||||
Portals []PortalConfig `json:"portals"`
|
||||
}
|
||||
|
||||
func (c *ReverseConfig) Build() (proto.Message, error) {
|
||||
config := &reverse.Config{}
|
||||
for _, bconfig := range c.Bridges {
|
||||
b, err := bconfig.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.BridgeConfig = append(config.BridgeConfig, b)
|
||||
}
|
||||
|
||||
for _, pconfig := range c.Portals {
|
||||
p, err := pconfig.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.PortalConfig = append(config.PortalConfig, p)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
45
infra/conf/reverse_test.go
Normal file
45
infra/conf/reverse_test.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/app/reverse"
|
||||
"github.com/xtls/xray-core/v1/infra/conf"
|
||||
)
|
||||
|
||||
func TestReverseConfig(t *testing.T) {
|
||||
creator := func() conf.Buildable {
|
||||
return new(conf.ReverseConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"bridges": [{
|
||||
"tag": "test",
|
||||
"domain": "test.example.com"
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &reverse.Config{
|
||||
BridgeConfig: []*reverse.BridgeConfig{
|
||||
{Tag: "test", Domain: "test.example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"portals": [{
|
||||
"tag": "test",
|
||||
"domain": "test.example.com"
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &reverse.Config{
|
||||
PortalConfig: []*reverse.PortalConfig{
|
||||
{Tag: "test", Domain: "test.example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
556
infra/conf/router.go
Normal file
556
infra/conf/router.go
Normal file
|
@ -0,0 +1,556 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/v1/app/router"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/platform/filesystem"
|
||||
)
|
||||
|
||||
type RouterRulesConfig struct {
|
||||
RuleList []json.RawMessage `json:"rules"`
|
||||
DomainStrategy string `json:"domainStrategy"`
|
||||
}
|
||||
|
||||
type BalancingRule struct {
|
||||
Tag string `json:"tag"`
|
||||
Selectors StringList `json:"selector"`
|
||||
}
|
||||
|
||||
func (r *BalancingRule) Build() (*router.BalancingRule, error) {
|
||||
if r.Tag == "" {
|
||||
return nil, newError("empty balancer tag")
|
||||
}
|
||||
if len(r.Selectors) == 0 {
|
||||
return nil, newError("empty selector list")
|
||||
}
|
||||
|
||||
return &router.BalancingRule{
|
||||
Tag: r.Tag,
|
||||
OutboundSelector: []string(r.Selectors),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RouterConfig struct {
|
||||
Settings *RouterRulesConfig `json:"settings"` // Deprecated
|
||||
RuleList []json.RawMessage `json:"rules"`
|
||||
DomainStrategy *string `json:"domainStrategy"`
|
||||
Balancers []*BalancingRule `json:"balancers"`
|
||||
}
|
||||
|
||||
func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
|
||||
ds := ""
|
||||
if c.DomainStrategy != nil {
|
||||
ds = *c.DomainStrategy
|
||||
} else if c.Settings != nil {
|
||||
ds = c.Settings.DomainStrategy
|
||||
}
|
||||
|
||||
switch strings.ToLower(ds) {
|
||||
case "alwaysip":
|
||||
return router.Config_UseIp
|
||||
case "ipifnonmatch":
|
||||
return router.Config_IpIfNonMatch
|
||||
case "ipondemand":
|
||||
return router.Config_IpOnDemand
|
||||
default:
|
||||
return router.Config_AsIs
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RouterConfig) Build() (*router.Config, error) {
|
||||
config := new(router.Config)
|
||||
config.DomainStrategy = c.getDomainStrategy()
|
||||
|
||||
var rawRuleList []json.RawMessage
|
||||
if c != nil {
|
||||
rawRuleList = c.RuleList
|
||||
if c.Settings != nil {
|
||||
c.RuleList = append(c.RuleList, c.Settings.RuleList...)
|
||||
rawRuleList = c.RuleList
|
||||
}
|
||||
}
|
||||
|
||||
for _, rawRule := range rawRuleList {
|
||||
rule, err := ParseRule(rawRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Rule = append(config.Rule, rule)
|
||||
}
|
||||
for _, rawBalancer := range c.Balancers {
|
||||
balancer, err := rawBalancer.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.BalancingRule = append(config.BalancingRule, balancer)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type RouterRule struct {
|
||||
Type string `json:"type"`
|
||||
OutboundTag string `json:"outboundTag"`
|
||||
BalancerTag string `json:"balancerTag"`
|
||||
}
|
||||
|
||||
func ParseIP(s string) (*router.CIDR, error) {
|
||||
var addr, mask string
|
||||
i := strings.Index(s, "/")
|
||||
if i < 0 {
|
||||
addr = s
|
||||
} else {
|
||||
addr = s[:i]
|
||||
mask = s[i+1:]
|
||||
}
|
||||
ip := net.ParseAddress(addr)
|
||||
switch ip.Family() {
|
||||
case net.AddressFamilyIPv4:
|
||||
bits := uint32(32)
|
||||
if len(mask) > 0 {
|
||||
bits64, err := strconv.ParseUint(mask, 10, 32)
|
||||
if err != nil {
|
||||
return nil, newError("invalid network mask for router: ", mask).Base(err)
|
||||
}
|
||||
bits = uint32(bits64)
|
||||
}
|
||||
if bits > 32 {
|
||||
return nil, newError("invalid network mask for router: ", bits)
|
||||
}
|
||||
return &router.CIDR{
|
||||
Ip: []byte(ip.IP()),
|
||||
Prefix: bits,
|
||||
}, nil
|
||||
case net.AddressFamilyIPv6:
|
||||
bits := uint32(128)
|
||||
if len(mask) > 0 {
|
||||
bits64, err := strconv.ParseUint(mask, 10, 32)
|
||||
if err != nil {
|
||||
return nil, newError("invalid network mask for router: ", mask).Base(err)
|
||||
}
|
||||
bits = uint32(bits64)
|
||||
}
|
||||
if bits > 128 {
|
||||
return nil, newError("invalid network mask for router: ", bits)
|
||||
}
|
||||
return &router.CIDR{
|
||||
Ip: []byte(ip.IP()),
|
||||
Prefix: bits,
|
||||
}, nil
|
||||
default:
|
||||
return nil, newError("unsupported address for router: ", s)
|
||||
}
|
||||
}
|
||||
|
||||
func loadGeoIP(country string) ([]*router.CIDR, error) {
|
||||
return loadIP("geoip.dat", country)
|
||||
}
|
||||
|
||||
func loadIP(filename, country string) ([]*router.CIDR, error) {
|
||||
geoipBytes, err := filesystem.ReadAsset(filename)
|
||||
if err != nil {
|
||||
return nil, newError("failed to open file: ", filename).Base(err)
|
||||
}
|
||||
var geoipList router.GeoIPList
|
||||
if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, geoip := range geoipList.Entry {
|
||||
if geoip.CountryCode == country {
|
||||
return geoip.Cidr, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, newError("country not found in ", filename, ": ", country)
|
||||
}
|
||||
|
||||
func loadSite(filename, country string) ([]*router.Domain, error) {
|
||||
geositeBytes, err := filesystem.ReadAsset(filename)
|
||||
if err != nil {
|
||||
return nil, newError("failed to open file: ", filename).Base(err)
|
||||
}
|
||||
var geositeList router.GeoSiteList
|
||||
if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, site := range geositeList.Entry {
|
||||
if site.CountryCode == country {
|
||||
return site.Domain, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, newError("list not found in ", filename, ": ", country)
|
||||
}
|
||||
|
||||
type AttributeMatcher interface {
|
||||
Match(*router.Domain) bool
|
||||
}
|
||||
|
||||
type BooleanMatcher string
|
||||
|
||||
func (m BooleanMatcher) Match(domain *router.Domain) bool {
|
||||
for _, attr := range domain.Attribute {
|
||||
if attr.Key == string(m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type AttributeList struct {
|
||||
matcher []AttributeMatcher
|
||||
}
|
||||
|
||||
func (al *AttributeList) Match(domain *router.Domain) bool {
|
||||
for _, matcher := range al.matcher {
|
||||
if !matcher.Match(domain) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (al *AttributeList) IsEmpty() bool {
|
||||
return len(al.matcher) == 0
|
||||
}
|
||||
|
||||
func parseAttrs(attrs []string) *AttributeList {
|
||||
al := new(AttributeList)
|
||||
for _, attr := range attrs {
|
||||
lc := strings.ToLower(attr)
|
||||
al.matcher = append(al.matcher, BooleanMatcher(lc))
|
||||
}
|
||||
return al
|
||||
}
|
||||
|
||||
func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) {
|
||||
parts := strings.Split(siteWithAttr, "@")
|
||||
if len(parts) == 0 {
|
||||
return nil, newError("empty site")
|
||||
}
|
||||
country := strings.ToUpper(parts[0])
|
||||
attrs := parseAttrs(parts[1:])
|
||||
domains, err := loadSite(file, country)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if attrs.IsEmpty() {
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
filteredDomains := make([]*router.Domain, 0, len(domains))
|
||||
for _, domain := range domains {
|
||||
if attrs.Match(domain) {
|
||||
filteredDomains = append(filteredDomains, domain)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredDomains, nil
|
||||
}
|
||||
|
||||
func parseDomainRule(domain string) ([]*router.Domain, error) {
|
||||
if strings.HasPrefix(domain, "geosite:") {
|
||||
country := strings.ToUpper(domain[8:])
|
||||
domains, err := loadGeositeWithAttr("geosite.dat", country)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load geosite: ", country).Base(err)
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
var isExtDatFile = 0
|
||||
{
|
||||
const prefix = "ext:"
|
||||
if strings.HasPrefix(domain, prefix) {
|
||||
isExtDatFile = len(prefix)
|
||||
}
|
||||
const prefixQualified = "ext-domain:"
|
||||
if strings.HasPrefix(domain, prefixQualified) {
|
||||
isExtDatFile = len(prefixQualified)
|
||||
}
|
||||
}
|
||||
if isExtDatFile != 0 {
|
||||
kv := strings.Split(domain[isExtDatFile:], ":")
|
||||
if len(kv) != 2 {
|
||||
return nil, newError("invalid external resource: ", domain)
|
||||
}
|
||||
filename := kv[0]
|
||||
country := kv[1]
|
||||
domains, err := loadGeositeWithAttr(filename, country)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load external sites: ", country, " from ", filename).Base(err)
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
domainRule := new(router.Domain)
|
||||
switch {
|
||||
case strings.HasPrefix(domain, "regexp:"):
|
||||
domainRule.Type = router.Domain_Regex
|
||||
domainRule.Value = domain[7:]
|
||||
|
||||
case strings.HasPrefix(domain, "domain:"):
|
||||
domainRule.Type = router.Domain_Domain
|
||||
domainRule.Value = domain[7:]
|
||||
|
||||
case strings.HasPrefix(domain, "full:"):
|
||||
domainRule.Type = router.Domain_Full
|
||||
domainRule.Value = domain[5:]
|
||||
|
||||
case strings.HasPrefix(domain, "keyword:"):
|
||||
domainRule.Type = router.Domain_Plain
|
||||
domainRule.Value = domain[8:]
|
||||
|
||||
case strings.HasPrefix(domain, "dotless:"):
|
||||
domainRule.Type = router.Domain_Regex
|
||||
switch substr := domain[8:]; {
|
||||
case substr == "":
|
||||
domainRule.Value = "^[^.]*$"
|
||||
case !strings.Contains(substr, "."):
|
||||
domainRule.Value = "^[^.]*" + substr + "[^.]*$"
|
||||
default:
|
||||
return nil, newError("substr in dotless rule should not contain a dot: ", substr)
|
||||
}
|
||||
|
||||
default:
|
||||
domainRule.Type = router.Domain_Plain
|
||||
domainRule.Value = domain
|
||||
}
|
||||
return []*router.Domain{domainRule}, nil
|
||||
}
|
||||
|
||||
func toCidrList(ips StringList) ([]*router.GeoIP, error) {
|
||||
var geoipList []*router.GeoIP
|
||||
var customCidrs []*router.CIDR
|
||||
|
||||
for _, ip := range ips {
|
||||
if strings.HasPrefix(ip, "geoip:") {
|
||||
country := ip[6:]
|
||||
geoip, err := loadGeoIP(strings.ToUpper(country))
|
||||
if err != nil {
|
||||
return nil, newError("failed to load GeoIP: ", country).Base(err)
|
||||
}
|
||||
|
||||
geoipList = append(geoipList, &router.GeoIP{
|
||||
CountryCode: strings.ToUpper(country),
|
||||
Cidr: geoip,
|
||||
})
|
||||
continue
|
||||
}
|
||||
var isExtDatFile = 0
|
||||
{
|
||||
const prefix = "ext:"
|
||||
if strings.HasPrefix(ip, prefix) {
|
||||
isExtDatFile = len(prefix)
|
||||
}
|
||||
const prefixQualified = "ext-ip:"
|
||||
if strings.HasPrefix(ip, prefixQualified) {
|
||||
isExtDatFile = len(prefixQualified)
|
||||
}
|
||||
}
|
||||
if isExtDatFile != 0 {
|
||||
kv := strings.Split(ip[isExtDatFile:], ":")
|
||||
if len(kv) != 2 {
|
||||
return nil, newError("invalid external resource: ", ip)
|
||||
}
|
||||
|
||||
filename := kv[0]
|
||||
country := kv[1]
|
||||
geoip, err := loadIP(filename, strings.ToUpper(country))
|
||||
if err != nil {
|
||||
return nil, newError("failed to load IPs: ", country, " from ", filename).Base(err)
|
||||
}
|
||||
|
||||
geoipList = append(geoipList, &router.GeoIP{
|
||||
CountryCode: strings.ToUpper(filename + "_" + country),
|
||||
Cidr: geoip,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
ipRule, err := ParseIP(ip)
|
||||
if err != nil {
|
||||
return nil, newError("invalid IP: ", ip).Base(err)
|
||||
}
|
||||
customCidrs = append(customCidrs, ipRule)
|
||||
}
|
||||
|
||||
if len(customCidrs) > 0 {
|
||||
geoipList = append(geoipList, &router.GeoIP{
|
||||
Cidr: customCidrs,
|
||||
})
|
||||
}
|
||||
|
||||
return geoipList, nil
|
||||
}
|
||||
|
||||
func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
|
||||
type RawFieldRule struct {
|
||||
RouterRule
|
||||
Domain *StringList `json:"domain"`
|
||||
IP *StringList `json:"ip"`
|
||||
Port *PortList `json:"port"`
|
||||
Network *NetworkList `json:"network"`
|
||||
SourceIP *StringList `json:"source"`
|
||||
SourcePort *PortList `json:"sourcePort"`
|
||||
User *StringList `json:"user"`
|
||||
InboundTag *StringList `json:"inboundTag"`
|
||||
Protocols *StringList `json:"protocol"`
|
||||
Attributes string `json:"attrs"`
|
||||
}
|
||||
rawFieldRule := new(RawFieldRule)
|
||||
err := json.Unmarshal(msg, rawFieldRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rule := new(router.RoutingRule)
|
||||
switch {
|
||||
case len(rawFieldRule.OutboundTag) > 0:
|
||||
rule.TargetTag = &router.RoutingRule_Tag{
|
||||
Tag: rawFieldRule.OutboundTag,
|
||||
}
|
||||
case len(rawFieldRule.BalancerTag) > 0:
|
||||
rule.TargetTag = &router.RoutingRule_BalancingTag{
|
||||
BalancingTag: rawFieldRule.BalancerTag,
|
||||
}
|
||||
default:
|
||||
return nil, newError("neither outboundTag nor balancerTag is specified in routing rule")
|
||||
}
|
||||
|
||||
if rawFieldRule.Domain != nil {
|
||||
for _, domain := range *rawFieldRule.Domain {
|
||||
rules, err := parseDomainRule(domain)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse domain rule: ", domain).Base(err)
|
||||
}
|
||||
rule.Domain = append(rule.Domain, rules...)
|
||||
}
|
||||
}
|
||||
|
||||
if rawFieldRule.IP != nil {
|
||||
geoipList, err := toCidrList(*rawFieldRule.IP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rule.Geoip = geoipList
|
||||
}
|
||||
|
||||
if rawFieldRule.Port != nil {
|
||||
rule.PortList = rawFieldRule.Port.Build()
|
||||
}
|
||||
|
||||
if rawFieldRule.Network != nil {
|
||||
rule.Networks = rawFieldRule.Network.Build()
|
||||
}
|
||||
|
||||
if rawFieldRule.SourceIP != nil {
|
||||
geoipList, err := toCidrList(*rawFieldRule.SourceIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rule.SourceGeoip = geoipList
|
||||
}
|
||||
|
||||
if rawFieldRule.SourcePort != nil {
|
||||
rule.SourcePortList = rawFieldRule.SourcePort.Build()
|
||||
}
|
||||
|
||||
if rawFieldRule.User != nil {
|
||||
for _, s := range *rawFieldRule.User {
|
||||
rule.UserEmail = append(rule.UserEmail, s)
|
||||
}
|
||||
}
|
||||
|
||||
if rawFieldRule.InboundTag != nil {
|
||||
for _, s := range *rawFieldRule.InboundTag {
|
||||
rule.InboundTag = append(rule.InboundTag, s)
|
||||
}
|
||||
}
|
||||
|
||||
if rawFieldRule.Protocols != nil {
|
||||
for _, s := range *rawFieldRule.Protocols {
|
||||
rule.Protocol = append(rule.Protocol, s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rawFieldRule.Attributes) > 0 {
|
||||
rule.Attributes = rawFieldRule.Attributes
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func ParseRule(msg json.RawMessage) (*router.RoutingRule, error) {
|
||||
rawRule := new(RouterRule)
|
||||
err := json.Unmarshal(msg, rawRule)
|
||||
if err != nil {
|
||||
return nil, newError("invalid router rule").Base(err)
|
||||
}
|
||||
if rawRule.Type == "field" {
|
||||
fieldrule, err := parseFieldRule(msg)
|
||||
if err != nil {
|
||||
return nil, newError("invalid field rule").Base(err)
|
||||
}
|
||||
return fieldrule, nil
|
||||
}
|
||||
if rawRule.Type == "chinaip" {
|
||||
chinaiprule, err := parseChinaIPRule(msg)
|
||||
if err != nil {
|
||||
return nil, newError("invalid chinaip rule").Base(err)
|
||||
}
|
||||
return chinaiprule, nil
|
||||
}
|
||||
if rawRule.Type == "chinasites" {
|
||||
chinasitesrule, err := parseChinaSitesRule(msg)
|
||||
if err != nil {
|
||||
return nil, newError("invalid chinasites rule").Base(err)
|
||||
}
|
||||
return chinasitesrule, nil
|
||||
}
|
||||
return nil, newError("unknown router rule type: ", rawRule.Type)
|
||||
}
|
||||
|
||||
func parseChinaIPRule(data []byte) (*router.RoutingRule, error) {
|
||||
rawRule := new(RouterRule)
|
||||
err := json.Unmarshal(data, rawRule)
|
||||
if err != nil {
|
||||
return nil, newError("invalid router rule").Base(err)
|
||||
}
|
||||
chinaIPs, err := loadGeoIP("CN")
|
||||
if err != nil {
|
||||
return nil, newError("failed to load geoip:cn").Base(err)
|
||||
}
|
||||
return &router.RoutingRule{
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: rawRule.OutboundTag,
|
||||
},
|
||||
Cidr: chinaIPs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseChinaSitesRule(data []byte) (*router.RoutingRule, error) {
|
||||
rawRule := new(RouterRule)
|
||||
err := json.Unmarshal(data, rawRule)
|
||||
if err != nil {
|
||||
return nil, newError("invalid router rule").Base(err).AtError()
|
||||
}
|
||||
domains, err := loadGeositeWithAttr("geosite.dat", "CN")
|
||||
if err != nil {
|
||||
return nil, newError("failed to load geosite:cn.").Base(err)
|
||||
}
|
||||
return &router.RoutingRule{
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: rawRule.OutboundTag,
|
||||
},
|
||||
Domain: domains,
|
||||
}, nil
|
||||
}
|
264
infra/conf/router_test.go
Normal file
264
infra/conf/router_test.go
Normal file
|
@ -0,0 +1,264 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/v1/app/router"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
)
|
||||
|
||||
func TestRouterConfig(t *testing.T) {
|
||||
createParser := func() func(string) (proto.Message, error) {
|
||||
return func(s string) (proto.Message, error) {
|
||||
config := new(RouterConfig)
|
||||
if err := json.Unmarshal([]byte(s), config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config.Build()
|
||||
}
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"strategy": "rules",
|
||||
"settings": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"domain": [
|
||||
"baidu.com",
|
||||
"qq.com"
|
||||
],
|
||||
"outboundTag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"ip": [
|
||||
"10.0.0.0/8",
|
||||
"::1/128"
|
||||
],
|
||||
"outboundTag": "test"
|
||||
},{
|
||||
"type": "field",
|
||||
"port": "53, 443, 1000-2000",
|
||||
"outboundTag": "test"
|
||||
},{
|
||||
"type": "field",
|
||||
"port": 123,
|
||||
"outboundTag": "test"
|
||||
}
|
||||
]
|
||||
},
|
||||
"balancers": [
|
||||
{
|
||||
"tag": "b1",
|
||||
"selector": ["test"]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
Parser: createParser(),
|
||||
Output: &router.Config{
|
||||
DomainStrategy: router.Config_AsIs,
|
||||
BalancingRule: []*router.BalancingRule{
|
||||
{
|
||||
Tag: "b1",
|
||||
OutboundSelector: []string{"test"},
|
||||
},
|
||||
},
|
||||
Rule: []*router.RoutingRule{
|
||||
{
|
||||
Domain: []*router.Domain{
|
||||
{
|
||||
Type: router.Domain_Plain,
|
||||
Value: "baidu.com",
|
||||
},
|
||||
{
|
||||
Type: router.Domain_Plain,
|
||||
Value: "qq.com",
|
||||
},
|
||||
},
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
Geoip: []*router.GeoIP{
|
||||
{
|
||||
Cidr: []*router.CIDR{
|
||||
{
|
||||
Ip: []byte{10, 0, 0, 0},
|
||||
Prefix: 8,
|
||||
},
|
||||
{
|
||||
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||||
Prefix: 128,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: "test",
|
||||
},
|
||||
},
|
||||
{
|
||||
PortList: &net.PortList{
|
||||
Range: []*net.PortRange{
|
||||
{From: 53, To: 53},
|
||||
{From: 443, To: 443},
|
||||
{From: 1000, To: 2000},
|
||||
},
|
||||
},
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: "test",
|
||||
},
|
||||
},
|
||||
{
|
||||
PortList: &net.PortList{
|
||||
Range: []*net.PortRange{
|
||||
{From: 123, To: 123},
|
||||
},
|
||||
},
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"strategy": "rules",
|
||||
"settings": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"domain": [
|
||||
"baidu.com",
|
||||
"qq.com"
|
||||
],
|
||||
"outboundTag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"ip": [
|
||||
"10.0.0.0/8",
|
||||
"::1/128"
|
||||
],
|
||||
"outboundTag": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
Parser: createParser(),
|
||||
Output: &router.Config{
|
||||
DomainStrategy: router.Config_IpIfNonMatch,
|
||||
Rule: []*router.RoutingRule{
|
||||
{
|
||||
Domain: []*router.Domain{
|
||||
{
|
||||
Type: router.Domain_Plain,
|
||||
Value: "baidu.com",
|
||||
},
|
||||
{
|
||||
Type: router.Domain_Plain,
|
||||
Value: "qq.com",
|
||||
},
|
||||
},
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
Geoip: []*router.GeoIP{
|
||||
{
|
||||
Cidr: []*router.CIDR{
|
||||
{
|
||||
Ip: []byte{10, 0, 0, 0},
|
||||
Prefix: 8,
|
||||
},
|
||||
{
|
||||
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||||
Prefix: 128,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"domain": [
|
||||
"baidu.com",
|
||||
"qq.com"
|
||||
],
|
||||
"outboundTag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"ip": [
|
||||
"10.0.0.0/8",
|
||||
"::1/128"
|
||||
],
|
||||
"outboundTag": "test"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
Parser: createParser(),
|
||||
Output: &router.Config{
|
||||
DomainStrategy: router.Config_AsIs,
|
||||
Rule: []*router.RoutingRule{
|
||||
{
|
||||
Domain: []*router.Domain{
|
||||
{
|
||||
Type: router.Domain_Plain,
|
||||
Value: "baidu.com",
|
||||
},
|
||||
{
|
||||
Type: router.Domain_Plain,
|
||||
Value: "qq.com",
|
||||
},
|
||||
},
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
Geoip: []*router.GeoIP{
|
||||
{
|
||||
Cidr: []*router.CIDR{
|
||||
{
|
||||
Ip: []byte{10, 0, 0, 0},
|
||||
Prefix: 8,
|
||||
},
|
||||
{
|
||||
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||||
Prefix: 128,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
9
infra/conf/serial/errors.generated.go
Normal file
9
infra/conf/serial/errors.generated.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package serial
|
||||
|
||||
import "github.com/xtls/xray-core/v1/common/errors"
|
||||
|
||||
type errPathObjHolder struct{}
|
||||
|
||||
func newError(values ...interface{}) *errors.Error {
|
||||
return errors.New(values...).WithPathObj(errPathObjHolder{})
|
||||
}
|
82
infra/conf/serial/loader.go
Normal file
82
infra/conf/serial/loader.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package serial
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/errors"
|
||||
"github.com/xtls/xray-core/v1/core"
|
||||
"github.com/xtls/xray-core/v1/infra/conf"
|
||||
json_reader "github.com/xtls/xray-core/v1/infra/conf/json"
|
||||
)
|
||||
|
||||
type offset struct {
|
||||
line int
|
||||
char int
|
||||
}
|
||||
|
||||
func findOffset(b []byte, o int) *offset {
|
||||
if o >= len(b) || o < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
line := 1
|
||||
char := 0
|
||||
for i, x := range b {
|
||||
if i == o {
|
||||
break
|
||||
}
|
||||
if x == '\n' {
|
||||
line++
|
||||
char = 0
|
||||
} else {
|
||||
char++
|
||||
}
|
||||
}
|
||||
|
||||
return &offset{line: line, char: char}
|
||||
}
|
||||
|
||||
// DecodeJSONConfig reads from reader and decode the config into *conf.Config
|
||||
// syntax error could be detected.
|
||||
func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) {
|
||||
jsonConfig := &conf.Config{}
|
||||
|
||||
jsonContent := bytes.NewBuffer(make([]byte, 0, 10240))
|
||||
jsonReader := io.TeeReader(&json_reader.Reader{
|
||||
Reader: reader,
|
||||
}, jsonContent)
|
||||
decoder := json.NewDecoder(jsonReader)
|
||||
|
||||
if err := decoder.Decode(jsonConfig); err != nil {
|
||||
var pos *offset
|
||||
cause := errors.Cause(err)
|
||||
switch tErr := cause.(type) {
|
||||
case *json.SyntaxError:
|
||||
pos = findOffset(jsonContent.Bytes(), int(tErr.Offset))
|
||||
case *json.UnmarshalTypeError:
|
||||
pos = findOffset(jsonContent.Bytes(), int(tErr.Offset))
|
||||
}
|
||||
if pos != nil {
|
||||
return nil, newError("failed to read config file at line ", pos.line, " char ", pos.char).Base(err)
|
||||
}
|
||||
return nil, newError("failed to read config file").Base(err)
|
||||
}
|
||||
|
||||
return jsonConfig, nil
|
||||
}
|
||||
|
||||
func LoadJSONConfig(reader io.Reader) (*core.Config, error) {
|
||||
jsonConfig, err := DecodeJSONConfig(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pbConfig, err := jsonConfig.Build()
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse json config").Base(err)
|
||||
}
|
||||
|
||||
return pbConfig, nil
|
||||
}
|
63
infra/conf/serial/loader_test.go
Normal file
63
infra/conf/serial/loader_test.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package serial_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/infra/conf/serial"
|
||||
)
|
||||
|
||||
func TestLoaderError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Input string
|
||||
Output string
|
||||
}{
|
||||
{
|
||||
Input: `{
|
||||
"log": {
|
||||
// abcd
|
||||
0,
|
||||
"loglevel": "info"
|
||||
}
|
||||
}`,
|
||||
Output: "line 4 char 6",
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"log": {
|
||||
// abcd
|
||||
"loglevel": "info",
|
||||
}
|
||||
}`,
|
||||
Output: "line 5 char 5",
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"port": 1,
|
||||
"inbounds": [{
|
||||
"protocol": "test"
|
||||
}]
|
||||
}`,
|
||||
Output: "parse json config",
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"inbounds": [{
|
||||
"port": 1,
|
||||
"listen": 0,
|
||||
"protocol": "test"
|
||||
}]
|
||||
}`,
|
||||
Output: "line 1 char 1",
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
reader := bytes.NewReader([]byte(testCase.Input))
|
||||
_, err := serial.LoadJSONConfig(reader)
|
||||
errString := err.Error()
|
||||
if !strings.Contains(errString, testCase.Output) {
|
||||
t.Error("unexpected output from json: ", testCase.Input, ". expected ", testCase.Output, ", but actually ", errString)
|
||||
}
|
||||
}
|
||||
}
|
3
infra/conf/serial/serial.go
Normal file
3
infra/conf/serial/serial.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package serial
|
||||
|
||||
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
|
128
infra/conf/shadowsocks.go
Normal file
128
infra/conf/shadowsocks.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/shadowsocks"
|
||||
)
|
||||
|
||||
func cipherFromString(c string) shadowsocks.CipherType {
|
||||
switch strings.ToLower(c) {
|
||||
case "aes-256-cfb":
|
||||
return shadowsocks.CipherType_AES_256_CFB
|
||||
case "aes-128-cfb":
|
||||
return shadowsocks.CipherType_AES_128_CFB
|
||||
case "chacha20":
|
||||
return shadowsocks.CipherType_CHACHA20
|
||||
case "chacha20-ietf":
|
||||
return shadowsocks.CipherType_CHACHA20_IETF
|
||||
case "aes-128-gcm", "aead_aes_128_gcm":
|
||||
return shadowsocks.CipherType_AES_128_GCM
|
||||
case "aes-256-gcm", "aead_aes_256_gcm":
|
||||
return shadowsocks.CipherType_AES_256_GCM
|
||||
case "chacha20-poly1305", "aead_chacha20_poly1305", "chacha20-ietf-poly1305":
|
||||
return shadowsocks.CipherType_CHACHA20_POLY1305
|
||||
case "none", "plain":
|
||||
return shadowsocks.CipherType_NONE
|
||||
default:
|
||||
return shadowsocks.CipherType_UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
type ShadowsocksServerConfig struct {
|
||||
Cipher string `json:"method"`
|
||||
Password string `json:"password"`
|
||||
UDP bool `json:"udp"`
|
||||
Level byte `json:"level"`
|
||||
Email string `json:"email"`
|
||||
NetworkList *NetworkList `json:"network"`
|
||||
}
|
||||
|
||||
func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
|
||||
config := new(shadowsocks.ServerConfig)
|
||||
config.UdpEnabled = v.UDP
|
||||
config.Network = v.NetworkList.Build()
|
||||
|
||||
if v.Password == "" {
|
||||
return nil, newError("Shadowsocks password is not specified.")
|
||||
}
|
||||
account := &shadowsocks.Account{
|
||||
Password: v.Password,
|
||||
}
|
||||
account.CipherType = cipherFromString(v.Cipher)
|
||||
if account.CipherType == shadowsocks.CipherType_UNKNOWN {
|
||||
return nil, newError("unknown cipher method: ", v.Cipher)
|
||||
}
|
||||
|
||||
config.User = &protocol.User{
|
||||
Email: v.Email,
|
||||
Level: uint32(v.Level),
|
||||
Account: serial.ToTypedMessage(account),
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type ShadowsocksServerTarget struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Cipher string `json:"method"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Ota bool `json:"ota"`
|
||||
Level byte `json:"level"`
|
||||
}
|
||||
|
||||
type ShadowsocksClientConfig struct {
|
||||
Servers []*ShadowsocksServerTarget `json:"servers"`
|
||||
}
|
||||
|
||||
func (v *ShadowsocksClientConfig) Build() (proto.Message, error) {
|
||||
config := new(shadowsocks.ClientConfig)
|
||||
|
||||
if len(v.Servers) == 0 {
|
||||
return nil, newError("0 Shadowsocks server configured.")
|
||||
}
|
||||
|
||||
serverSpecs := make([]*protocol.ServerEndpoint, len(v.Servers))
|
||||
for idx, server := range v.Servers {
|
||||
if server.Address == nil {
|
||||
return nil, newError("Shadowsocks server address is not set.")
|
||||
}
|
||||
if server.Port == 0 {
|
||||
return nil, newError("Invalid Shadowsocks port.")
|
||||
}
|
||||
if server.Password == "" {
|
||||
return nil, newError("Shadowsocks password is not specified.")
|
||||
}
|
||||
account := &shadowsocks.Account{
|
||||
Password: server.Password,
|
||||
}
|
||||
account.CipherType = cipherFromString(server.Cipher)
|
||||
if account.CipherType == shadowsocks.CipherType_UNKNOWN {
|
||||
return nil, newError("unknown cipher method: ", server.Cipher)
|
||||
}
|
||||
|
||||
ss := &protocol.ServerEndpoint{
|
||||
Address: server.Address.Build(),
|
||||
Port: uint32(server.Port),
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Level: uint32(server.Level),
|
||||
Email: server.Email,
|
||||
Account: serial.ToTypedMessage(account),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
serverSpecs[idx] = ss
|
||||
}
|
||||
|
||||
config.Server = serverSpecs
|
||||
|
||||
return config, nil
|
||||
}
|
36
infra/conf/shadowsocks_test.go
Normal file
36
infra/conf/shadowsocks_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/shadowsocks"
|
||||
)
|
||||
|
||||
func TestShadowsocksServerConfigParsing(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(ShadowsocksServerConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"method": "aes-128-cfb",
|
||||
"password": "xray-password"
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &shadowsocks.ServerConfig{
|
||||
User: &protocol.User{
|
||||
Account: serial.ToTypedMessage(&shadowsocks.Account{
|
||||
CipherType: shadowsocks.CipherType_AES_128_CFB,
|
||||
Password: "xray-password",
|
||||
}),
|
||||
},
|
||||
Network: []net.Network{net.Network_TCP},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
99
infra/conf/socks.go
Normal file
99
infra/conf/socks.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/socks"
|
||||
)
|
||||
|
||||
type SocksAccount struct {
|
||||
Username string `json:"user"`
|
||||
Password string `json:"pass"`
|
||||
}
|
||||
|
||||
func (v *SocksAccount) Build() *socks.Account {
|
||||
return &socks.Account{
|
||||
Username: v.Username,
|
||||
Password: v.Password,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
AuthMethodNoAuth = "noauth"
|
||||
AuthMethodUserPass = "password"
|
||||
)
|
||||
|
||||
type SocksServerConfig struct {
|
||||
AuthMethod string `json:"auth"`
|
||||
Accounts []*SocksAccount `json:"accounts"`
|
||||
UDP bool `json:"udp"`
|
||||
Host *Address `json:"ip"`
|
||||
Timeout uint32 `json:"timeout"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
}
|
||||
|
||||
func (v *SocksServerConfig) Build() (proto.Message, error) {
|
||||
config := new(socks.ServerConfig)
|
||||
switch v.AuthMethod {
|
||||
case AuthMethodNoAuth:
|
||||
config.AuthType = socks.AuthType_NO_AUTH
|
||||
case AuthMethodUserPass:
|
||||
config.AuthType = socks.AuthType_PASSWORD
|
||||
default:
|
||||
// newError("unknown socks auth method: ", v.AuthMethod, ". Default to noauth.").AtWarning().WriteToLog()
|
||||
config.AuthType = socks.AuthType_NO_AUTH
|
||||
}
|
||||
|
||||
if len(v.Accounts) > 0 {
|
||||
config.Accounts = make(map[string]string, len(v.Accounts))
|
||||
for _, account := range v.Accounts {
|
||||
config.Accounts[account.Username] = account.Password
|
||||
}
|
||||
}
|
||||
|
||||
config.UdpEnabled = v.UDP
|
||||
if v.Host != nil {
|
||||
config.Address = v.Host.Build()
|
||||
}
|
||||
|
||||
config.Timeout = v.Timeout
|
||||
config.UserLevel = v.UserLevel
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type SocksRemoteConfig struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Users []json.RawMessage `json:"users"`
|
||||
}
|
||||
type SocksClientConfig struct {
|
||||
Servers []*SocksRemoteConfig `json:"servers"`
|
||||
}
|
||||
|
||||
func (v *SocksClientConfig) Build() (proto.Message, error) {
|
||||
config := new(socks.ClientConfig)
|
||||
config.Server = make([]*protocol.ServerEndpoint, len(v.Servers))
|
||||
for idx, serverConfig := range v.Servers {
|
||||
server := &protocol.ServerEndpoint{
|
||||
Address: serverConfig.Address.Build(),
|
||||
Port: uint32(serverConfig.Port),
|
||||
}
|
||||
for _, rawUser := range serverConfig.Users {
|
||||
user := new(protocol.User)
|
||||
if err := json.Unmarshal(rawUser, user); err != nil {
|
||||
return nil, newError("failed to parse Socks user").Base(err).AtError()
|
||||
}
|
||||
account := new(SocksAccount)
|
||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||
return nil, newError("failed to parse socks account").Base(err).AtError()
|
||||
}
|
||||
user.Account = serial.ToTypedMessage(account.Build())
|
||||
server.User = append(server.User, user)
|
||||
}
|
||||
config.Server[idx] = server
|
||||
}
|
||||
return config, nil
|
||||
}
|
92
infra/conf/socks_test.go
Normal file
92
infra/conf/socks_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/socks"
|
||||
)
|
||||
|
||||
func TestSocksInboundConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(SocksServerConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"auth": "password",
|
||||
"accounts": [
|
||||
{
|
||||
"user": "my-username",
|
||||
"pass": "my-password"
|
||||
}
|
||||
],
|
||||
"udp": false,
|
||||
"ip": "127.0.0.1",
|
||||
"timeout": 5,
|
||||
"userLevel": 1
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &socks.ServerConfig{
|
||||
AuthType: socks.AuthType_PASSWORD,
|
||||
Accounts: map[string]string{
|
||||
"my-username": "my-password",
|
||||
},
|
||||
UdpEnabled: false,
|
||||
Address: &net.IPOrDomain{
|
||||
Address: &net.IPOrDomain_Ip{
|
||||
Ip: []byte{127, 0, 0, 1},
|
||||
},
|
||||
},
|
||||
Timeout: 5,
|
||||
UserLevel: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSocksOutboundConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(SocksClientConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"servers": [{
|
||||
"address": "127.0.0.1",
|
||||
"port": 1234,
|
||||
"users": [
|
||||
{"user": "test user", "pass": "test pass", "email": "test@email.com"}
|
||||
]
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &socks.ClientConfig{
|
||||
Server: []*protocol.ServerEndpoint{
|
||||
{
|
||||
Address: &net.IPOrDomain{
|
||||
Address: &net.IPOrDomain_Ip{
|
||||
Ip: []byte{127, 0, 0, 1},
|
||||
},
|
||||
},
|
||||
Port: 1234,
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Email: "test@email.com",
|
||||
Account: serial.ToTypedMessage(&socks.Account{
|
||||
Username: "test user",
|
||||
Password: "test pass",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
89
infra/conf/transport.go
Normal file
89
infra/conf/transport.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/transport"
|
||||
"github.com/xtls/xray-core/v1/transport/internet"
|
||||
)
|
||||
|
||||
type TransportConfig struct {
|
||||
TCPConfig *TCPConfig `json:"tcpSettings"`
|
||||
KCPConfig *KCPConfig `json:"kcpSettings"`
|
||||
WSConfig *WebSocketConfig `json:"wsSettings"`
|
||||
HTTPConfig *HTTPConfig `json:"httpSettings"`
|
||||
DSConfig *DomainSocketConfig `json:"dsSettings"`
|
||||
QUICConfig *QUICConfig `json:"quicSettings"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *TransportConfig) Build() (*transport.Config, error) {
|
||||
config := new(transport.Config)
|
||||
|
||||
if c.TCPConfig != nil {
|
||||
ts, err := c.TCPConfig.Build()
|
||||
if err != nil {
|
||||
return nil, newError("failed to build TCP config").Base(err).AtError()
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "tcp",
|
||||
Settings: serial.ToTypedMessage(ts),
|
||||
})
|
||||
}
|
||||
|
||||
if c.KCPConfig != nil {
|
||||
ts, err := c.KCPConfig.Build()
|
||||
if err != nil {
|
||||
return nil, newError("failed to build mKCP config").Base(err).AtError()
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "mkcp",
|
||||
Settings: serial.ToTypedMessage(ts),
|
||||
})
|
||||
}
|
||||
|
||||
if c.WSConfig != nil {
|
||||
ts, err := c.WSConfig.Build()
|
||||
if err != nil {
|
||||
return nil, newError("failed to build WebSocket config").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "websocket",
|
||||
Settings: serial.ToTypedMessage(ts),
|
||||
})
|
||||
}
|
||||
|
||||
if c.HTTPConfig != nil {
|
||||
ts, err := c.HTTPConfig.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build HTTP config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "http",
|
||||
Settings: serial.ToTypedMessage(ts),
|
||||
})
|
||||
}
|
||||
|
||||
if c.DSConfig != nil {
|
||||
ds, err := c.DSConfig.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build DomainSocket config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "domainsocket",
|
||||
Settings: serial.ToTypedMessage(ds),
|
||||
})
|
||||
}
|
||||
|
||||
if c.QUICConfig != nil {
|
||||
qs, err := c.QUICConfig.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build QUIC config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "quic",
|
||||
Settings: serial.ToTypedMessage(qs),
|
||||
})
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
223
infra/conf/transport_authenticators.go
Normal file
223
infra/conf/transport_authenticators.go
Normal file
|
@ -0,0 +1,223 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/http"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/noop"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/srtp"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/tls"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/utp"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/wechat"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/wireguard"
|
||||
)
|
||||
|
||||
type NoOpAuthenticator struct{}
|
||||
|
||||
func (NoOpAuthenticator) Build() (proto.Message, error) {
|
||||
return new(noop.Config), nil
|
||||
}
|
||||
|
||||
type NoOpConnectionAuthenticator struct{}
|
||||
|
||||
func (NoOpConnectionAuthenticator) Build() (proto.Message, error) {
|
||||
return new(noop.ConnectionConfig), nil
|
||||
}
|
||||
|
||||
type SRTPAuthenticator struct{}
|
||||
|
||||
func (SRTPAuthenticator) Build() (proto.Message, error) {
|
||||
return new(srtp.Config), nil
|
||||
}
|
||||
|
||||
type UTPAuthenticator struct{}
|
||||
|
||||
func (UTPAuthenticator) Build() (proto.Message, error) {
|
||||
return new(utp.Config), nil
|
||||
}
|
||||
|
||||
type WechatVideoAuthenticator struct{}
|
||||
|
||||
func (WechatVideoAuthenticator) Build() (proto.Message, error) {
|
||||
return new(wechat.VideoConfig), nil
|
||||
}
|
||||
|
||||
type WireguardAuthenticator struct{}
|
||||
|
||||
func (WireguardAuthenticator) Build() (proto.Message, error) {
|
||||
return new(wireguard.WireguardConfig), nil
|
||||
}
|
||||
|
||||
type DTLSAuthenticator struct{}
|
||||
|
||||
func (DTLSAuthenticator) Build() (proto.Message, error) {
|
||||
return new(tls.PacketConfig), nil
|
||||
}
|
||||
|
||||
type AuthenticatorRequest struct {
|
||||
Version string `json:"version"`
|
||||
Method string `json:"method"`
|
||||
Path StringList `json:"path"`
|
||||
Headers map[string]*StringList `json:"headers"`
|
||||
}
|
||||
|
||||
func sortMapKeys(m map[string]*StringList) []string {
|
||||
var keys []string
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
|
||||
config := &http.RequestConfig{
|
||||
Uri: []string{"/"},
|
||||
Header: []*http.Header{
|
||||
{
|
||||
Name: "Host",
|
||||
Value: []string{"www.baidu.com", "www.bing.com"},
|
||||
},
|
||||
{
|
||||
Name: "User-Agent",
|
||||
Value: []string{
|
||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Accept-Encoding",
|
||||
Value: []string{"gzip, deflate"},
|
||||
},
|
||||
{
|
||||
Name: "Connection",
|
||||
Value: []string{"keep-alive"},
|
||||
},
|
||||
{
|
||||
Name: "Pragma",
|
||||
Value: []string{"no-cache"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(v.Version) > 0 {
|
||||
config.Version = &http.Version{Value: v.Version}
|
||||
}
|
||||
|
||||
if len(v.Method) > 0 {
|
||||
config.Method = &http.Method{Value: v.Method}
|
||||
}
|
||||
|
||||
if len(v.Path) > 0 {
|
||||
config.Uri = append([]string(nil), (v.Path)...)
|
||||
}
|
||||
|
||||
if len(v.Headers) > 0 {
|
||||
config.Header = make([]*http.Header, 0, len(v.Headers))
|
||||
headerNames := sortMapKeys(v.Headers)
|
||||
for _, key := range headerNames {
|
||||
value := v.Headers[key]
|
||||
if value == nil {
|
||||
return nil, newError("empty HTTP header value: " + key).AtError()
|
||||
}
|
||||
config.Header = append(config.Header, &http.Header{
|
||||
Name: key,
|
||||
Value: append([]string(nil), (*value)...),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type AuthenticatorResponse struct {
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
Headers map[string]*StringList `json:"headers"`
|
||||
}
|
||||
|
||||
func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
|
||||
config := &http.ResponseConfig{
|
||||
Header: []*http.Header{
|
||||
{
|
||||
Name: "Content-Type",
|
||||
Value: []string{"application/octet-stream", "video/mpeg"},
|
||||
},
|
||||
{
|
||||
Name: "Transfer-Encoding",
|
||||
Value: []string{"chunked"},
|
||||
},
|
||||
{
|
||||
Name: "Connection",
|
||||
Value: []string{"keep-alive"},
|
||||
},
|
||||
{
|
||||
Name: "Pragma",
|
||||
Value: []string{"no-cache"},
|
||||
},
|
||||
{
|
||||
Name: "Cache-Control",
|
||||
Value: []string{"private", "no-cache"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(v.Version) > 0 {
|
||||
config.Version = &http.Version{Value: v.Version}
|
||||
}
|
||||
|
||||
if len(v.Status) > 0 || len(v.Reason) > 0 {
|
||||
config.Status = &http.Status{
|
||||
Code: "200",
|
||||
Reason: "OK",
|
||||
}
|
||||
if len(v.Status) > 0 {
|
||||
config.Status.Code = v.Status
|
||||
}
|
||||
if len(v.Reason) > 0 {
|
||||
config.Status.Reason = v.Reason
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.Headers) > 0 {
|
||||
config.Header = make([]*http.Header, 0, len(v.Headers))
|
||||
headerNames := sortMapKeys(v.Headers)
|
||||
for _, key := range headerNames {
|
||||
value := v.Headers[key]
|
||||
if value == nil {
|
||||
return nil, newError("empty HTTP header value: " + key).AtError()
|
||||
}
|
||||
config.Header = append(config.Header, &http.Header{
|
||||
Name: key,
|
||||
Value: append([]string(nil), (*value)...),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
Request AuthenticatorRequest `json:"request"`
|
||||
Response AuthenticatorResponse `json:"response"`
|
||||
}
|
||||
|
||||
func (v *Authenticator) Build() (proto.Message, error) {
|
||||
config := new(http.Config)
|
||||
requestConfig, err := v.Request.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Request = requestConfig
|
||||
|
||||
responseConfig, err := v.Response.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Response = responseConfig
|
||||
|
||||
return config, nil
|
||||
}
|
599
infra/conf/transport_internet.go
Normal file
599
infra/conf/transport_internet.go
Normal file
|
@ -0,0 +1,599 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/xtls/xray-core/v1/common/platform/filesystem"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/transport/internet"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/domainsocket"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/http"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/kcp"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/quic"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/tcp"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/tls"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/websocket"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/xtls"
|
||||
)
|
||||
|
||||
var (
|
||||
kcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"none": func() interface{} { return new(NoOpAuthenticator) },
|
||||
"srtp": func() interface{} { return new(SRTPAuthenticator) },
|
||||
"utp": func() interface{} { return new(UTPAuthenticator) },
|
||||
"wechat-video": func() interface{} { return new(WechatVideoAuthenticator) },
|
||||
"dtls": func() interface{} { return new(DTLSAuthenticator) },
|
||||
"wireguard": func() interface{} { return new(WireguardAuthenticator) },
|
||||
}, "type", "")
|
||||
|
||||
tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"none": func() interface{} { return new(NoOpConnectionAuthenticator) },
|
||||
"http": func() interface{} { return new(Authenticator) },
|
||||
}, "type", "")
|
||||
)
|
||||
|
||||
type KCPConfig struct {
|
||||
Mtu *uint32 `json:"mtu"`
|
||||
Tti *uint32 `json:"tti"`
|
||||
UpCap *uint32 `json:"uplinkCapacity"`
|
||||
DownCap *uint32 `json:"downlinkCapacity"`
|
||||
Congestion *bool `json:"congestion"`
|
||||
ReadBufferSize *uint32 `json:"readBufferSize"`
|
||||
WriteBufferSize *uint32 `json:"writeBufferSize"`
|
||||
HeaderConfig json.RawMessage `json:"header"`
|
||||
Seed *string `json:"seed"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *KCPConfig) Build() (proto.Message, error) {
|
||||
config := new(kcp.Config)
|
||||
|
||||
if c.Mtu != nil {
|
||||
mtu := *c.Mtu
|
||||
if mtu < 576 || mtu > 1460 {
|
||||
return nil, newError("invalid mKCP MTU size: ", mtu).AtError()
|
||||
}
|
||||
config.Mtu = &kcp.MTU{Value: mtu}
|
||||
}
|
||||
if c.Tti != nil {
|
||||
tti := *c.Tti
|
||||
if tti < 10 || tti > 100 {
|
||||
return nil, newError("invalid mKCP TTI: ", tti).AtError()
|
||||
}
|
||||
config.Tti = &kcp.TTI{Value: tti}
|
||||
}
|
||||
if c.UpCap != nil {
|
||||
config.UplinkCapacity = &kcp.UplinkCapacity{Value: *c.UpCap}
|
||||
}
|
||||
if c.DownCap != nil {
|
||||
config.DownlinkCapacity = &kcp.DownlinkCapacity{Value: *c.DownCap}
|
||||
}
|
||||
if c.Congestion != nil {
|
||||
config.Congestion = *c.Congestion
|
||||
}
|
||||
if c.ReadBufferSize != nil {
|
||||
size := *c.ReadBufferSize
|
||||
if size > 0 {
|
||||
config.ReadBuffer = &kcp.ReadBuffer{Size: size * 1024 * 1024}
|
||||
} else {
|
||||
config.ReadBuffer = &kcp.ReadBuffer{Size: 512 * 1024}
|
||||
}
|
||||
}
|
||||
if c.WriteBufferSize != nil {
|
||||
size := *c.WriteBufferSize
|
||||
if size > 0 {
|
||||
config.WriteBuffer = &kcp.WriteBuffer{Size: size * 1024 * 1024}
|
||||
} else {
|
||||
config.WriteBuffer = &kcp.WriteBuffer{Size: 512 * 1024}
|
||||
}
|
||||
}
|
||||
if len(c.HeaderConfig) > 0 {
|
||||
headerConfig, _, err := kcpHeaderLoader.Load(c.HeaderConfig)
|
||||
if err != nil {
|
||||
return nil, newError("invalid mKCP header config.").Base(err).AtError()
|
||||
}
|
||||
ts, err := headerConfig.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, newError("invalid mKCP header config").Base(err).AtError()
|
||||
}
|
||||
config.HeaderConfig = serial.ToTypedMessage(ts)
|
||||
}
|
||||
|
||||
if c.Seed != nil {
|
||||
config.Seed = &kcp.EncryptionSeed{Seed: *c.Seed}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type TCPConfig struct {
|
||||
HeaderConfig json.RawMessage `json:"header"`
|
||||
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *TCPConfig) Build() (proto.Message, error) {
|
||||
config := new(tcp.Config)
|
||||
if len(c.HeaderConfig) > 0 {
|
||||
headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig)
|
||||
if err != nil {
|
||||
return nil, newError("invalid TCP header config").Base(err).AtError()
|
||||
}
|
||||
ts, err := headerConfig.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, newError("invalid TCP header config").Base(err).AtError()
|
||||
}
|
||||
config.HeaderSettings = serial.ToTypedMessage(ts)
|
||||
}
|
||||
if c.AcceptProxyProtocol {
|
||||
config.AcceptProxyProtocol = c.AcceptProxyProtocol
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type WebSocketConfig struct {
|
||||
Path string `json:"path"`
|
||||
Path2 string `json:"Path"` // The key was misspelled. For backward compatibility, we have to keep track the old key.
|
||||
Headers map[string]string `json:"headers"`
|
||||
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *WebSocketConfig) Build() (proto.Message, error) {
|
||||
path := c.Path
|
||||
if path == "" && c.Path2 != "" {
|
||||
path = c.Path2
|
||||
}
|
||||
header := make([]*websocket.Header, 0, 32)
|
||||
for key, value := range c.Headers {
|
||||
header = append(header, &websocket.Header{
|
||||
Key: key,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
config := &websocket.Config{
|
||||
Path: path,
|
||||
Header: header,
|
||||
}
|
||||
if c.AcceptProxyProtocol {
|
||||
config.AcceptProxyProtocol = c.AcceptProxyProtocol
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Host *StringList `json:"host"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *HTTPConfig) Build() (proto.Message, error) {
|
||||
config := &http.Config{
|
||||
Path: c.Path,
|
||||
}
|
||||
if c.Host != nil {
|
||||
config.Host = []string(*c.Host)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type QUICConfig struct {
|
||||
Header json.RawMessage `json:"header"`
|
||||
Security string `json:"security"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *QUICConfig) Build() (proto.Message, error) {
|
||||
config := &quic.Config{
|
||||
Key: c.Key,
|
||||
}
|
||||
|
||||
if len(c.Header) > 0 {
|
||||
headerConfig, _, err := kcpHeaderLoader.Load(c.Header)
|
||||
if err != nil {
|
||||
return nil, newError("invalid QUIC header config.").Base(err).AtError()
|
||||
}
|
||||
ts, err := headerConfig.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, newError("invalid QUIC header config").Base(err).AtError()
|
||||
}
|
||||
config.Header = serial.ToTypedMessage(ts)
|
||||
}
|
||||
|
||||
var st protocol.SecurityType
|
||||
switch strings.ToLower(c.Security) {
|
||||
case "aes-128-gcm":
|
||||
st = protocol.SecurityType_AES128_GCM
|
||||
case "chacha20-poly1305":
|
||||
st = protocol.SecurityType_CHACHA20_POLY1305
|
||||
default:
|
||||
st = protocol.SecurityType_NONE
|
||||
}
|
||||
|
||||
config.Security = &protocol.SecurityConfig{
|
||||
Type: st,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type DomainSocketConfig struct {
|
||||
Path string `json:"path"`
|
||||
Abstract bool `json:"abstract"`
|
||||
Padding bool `json:"padding"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *DomainSocketConfig) Build() (proto.Message, error) {
|
||||
return &domainsocket.Config{
|
||||
Path: c.Path,
|
||||
Abstract: c.Abstract,
|
||||
Padding: c.Padding,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readFileOrString(f string, s []string) ([]byte, error) {
|
||||
if len(f) > 0 {
|
||||
return filesystem.ReadFile(f)
|
||||
}
|
||||
if len(s) > 0 {
|
||||
return []byte(strings.Join(s, "\n")), nil
|
||||
}
|
||||
return nil, newError("both file and bytes are empty.")
|
||||
}
|
||||
|
||||
type TLSCertConfig struct {
|
||||
CertFile string `json:"certificateFile"`
|
||||
CertStr []string `json:"certificate"`
|
||||
KeyFile string `json:"keyFile"`
|
||||
KeyStr []string `json:"key"`
|
||||
Usage string `json:"usage"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *TLSCertConfig) Build() (*tls.Certificate, error) {
|
||||
certificate := new(tls.Certificate)
|
||||
|
||||
cert, err := readFileOrString(c.CertFile, c.CertStr)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse certificate").Base(err)
|
||||
}
|
||||
certificate.Certificate = cert
|
||||
|
||||
if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 {
|
||||
key, err := readFileOrString(c.KeyFile, c.KeyStr)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse key").Base(err)
|
||||
}
|
||||
certificate.Key = key
|
||||
}
|
||||
|
||||
switch strings.ToLower(c.Usage) {
|
||||
case "encipherment":
|
||||
certificate.Usage = tls.Certificate_ENCIPHERMENT
|
||||
case "verify":
|
||||
certificate.Usage = tls.Certificate_AUTHORITY_VERIFY
|
||||
case "issue":
|
||||
certificate.Usage = tls.Certificate_AUTHORITY_ISSUE
|
||||
default:
|
||||
certificate.Usage = tls.Certificate_ENCIPHERMENT
|
||||
}
|
||||
|
||||
return certificate, nil
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
Insecure bool `json:"allowInsecure"`
|
||||
InsecureCiphers bool `json:"allowInsecureCiphers"`
|
||||
Certs []*TLSCertConfig `json:"certificates"`
|
||||
ServerName string `json:"serverName"`
|
||||
ALPN *StringList `json:"alpn"`
|
||||
DisableSessionResumption bool `json:"disableSessionResumption"`
|
||||
DisableSystemRoot bool `json:"disableSystemRoot"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *TLSConfig) Build() (proto.Message, error) {
|
||||
config := new(tls.Config)
|
||||
config.Certificate = make([]*tls.Certificate, len(c.Certs))
|
||||
for idx, certConf := range c.Certs {
|
||||
cert, err := certConf.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Certificate[idx] = cert
|
||||
}
|
||||
serverName := c.ServerName
|
||||
config.AllowInsecure = c.Insecure
|
||||
config.AllowInsecureCiphers = c.InsecureCiphers
|
||||
if len(c.ServerName) > 0 {
|
||||
config.ServerName = serverName
|
||||
}
|
||||
if c.ALPN != nil && len(*c.ALPN) > 0 {
|
||||
config.NextProtocol = []string(*c.ALPN)
|
||||
}
|
||||
config.DisableSessionResumption = c.DisableSessionResumption
|
||||
config.DisableSystemRoot = c.DisableSystemRoot
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type XTLSCertConfig struct {
|
||||
CertFile string `json:"certificateFile"`
|
||||
CertStr []string `json:"certificate"`
|
||||
KeyFile string `json:"keyFile"`
|
||||
KeyStr []string `json:"key"`
|
||||
Usage string `json:"usage"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *XTLSCertConfig) Build() (*xtls.Certificate, error) {
|
||||
certificate := new(xtls.Certificate)
|
||||
|
||||
cert, err := readFileOrString(c.CertFile, c.CertStr)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse certificate").Base(err)
|
||||
}
|
||||
certificate.Certificate = cert
|
||||
|
||||
if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 {
|
||||
key, err := readFileOrString(c.KeyFile, c.KeyStr)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse key").Base(err)
|
||||
}
|
||||
certificate.Key = key
|
||||
}
|
||||
|
||||
switch strings.ToLower(c.Usage) {
|
||||
case "encipherment":
|
||||
certificate.Usage = xtls.Certificate_ENCIPHERMENT
|
||||
case "verify":
|
||||
certificate.Usage = xtls.Certificate_AUTHORITY_VERIFY
|
||||
case "issue":
|
||||
certificate.Usage = xtls.Certificate_AUTHORITY_ISSUE
|
||||
default:
|
||||
certificate.Usage = xtls.Certificate_ENCIPHERMENT
|
||||
}
|
||||
|
||||
return certificate, nil
|
||||
}
|
||||
|
||||
type XTLSConfig struct {
|
||||
Insecure bool `json:"allowInsecure"`
|
||||
InsecureCiphers bool `json:"allowInsecureCiphers"`
|
||||
Certs []*XTLSCertConfig `json:"certificates"`
|
||||
ServerName string `json:"serverName"`
|
||||
ALPN *StringList `json:"alpn"`
|
||||
DisableSessionResumption bool `json:"disableSessionResumption"`
|
||||
DisableSystemRoot bool `json:"disableSystemRoot"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *XTLSConfig) Build() (proto.Message, error) {
|
||||
config := new(xtls.Config)
|
||||
config.Certificate = make([]*xtls.Certificate, len(c.Certs))
|
||||
for idx, certConf := range c.Certs {
|
||||
cert, err := certConf.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Certificate[idx] = cert
|
||||
}
|
||||
serverName := c.ServerName
|
||||
config.AllowInsecure = c.Insecure
|
||||
config.AllowInsecureCiphers = c.InsecureCiphers
|
||||
if len(c.ServerName) > 0 {
|
||||
config.ServerName = serverName
|
||||
}
|
||||
if c.ALPN != nil && len(*c.ALPN) > 0 {
|
||||
config.NextProtocol = []string(*c.ALPN)
|
||||
}
|
||||
config.DisableSessionResumption = c.DisableSessionResumption
|
||||
config.DisableSystemRoot = c.DisableSystemRoot
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type TransportProtocol string
|
||||
|
||||
// Build implements Buildable.
|
||||
func (p TransportProtocol) Build() (string, error) {
|
||||
switch strings.ToLower(string(p)) {
|
||||
case "tcp":
|
||||
return "tcp", nil
|
||||
case "kcp", "mkcp":
|
||||
return "mkcp", nil
|
||||
case "ws", "websocket":
|
||||
return "websocket", nil
|
||||
case "h2", "http":
|
||||
return "http", nil
|
||||
case "ds", "domainsocket":
|
||||
return "domainsocket", nil
|
||||
case "quic":
|
||||
return "quic", nil
|
||||
default:
|
||||
return "", newError("Config: unknown transport protocol: ", p)
|
||||
}
|
||||
}
|
||||
|
||||
type SocketConfig struct {
|
||||
Mark int32 `json:"mark"`
|
||||
TFO *bool `json:"tcpFastOpen"`
|
||||
TProxy string `json:"tproxy"`
|
||||
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
|
||||
var tfoSettings internet.SocketConfig_TCPFastOpenState
|
||||
if c.TFO != nil {
|
||||
if *c.TFO {
|
||||
tfoSettings = internet.SocketConfig_Enable
|
||||
} else {
|
||||
tfoSettings = internet.SocketConfig_Disable
|
||||
}
|
||||
}
|
||||
var tproxy internet.SocketConfig_TProxyMode
|
||||
switch strings.ToLower(c.TProxy) {
|
||||
case "tproxy":
|
||||
tproxy = internet.SocketConfig_TProxy
|
||||
case "redirect":
|
||||
tproxy = internet.SocketConfig_Redirect
|
||||
default:
|
||||
tproxy = internet.SocketConfig_Off
|
||||
}
|
||||
|
||||
return &internet.SocketConfig{
|
||||
Mark: c.Mark,
|
||||
Tfo: tfoSettings,
|
||||
Tproxy: tproxy,
|
||||
AcceptProxyProtocol: c.AcceptProxyProtocol,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type StreamConfig struct {
|
||||
Network *TransportProtocol `json:"network"`
|
||||
Security string `json:"security"`
|
||||
TLSSettings *TLSConfig `json:"tlsSettings"`
|
||||
XTLSSettings *XTLSConfig `json:"xtlsSettings"`
|
||||
TCPSettings *TCPConfig `json:"tcpSettings"`
|
||||
KCPSettings *KCPConfig `json:"kcpSettings"`
|
||||
WSSettings *WebSocketConfig `json:"wsSettings"`
|
||||
HTTPSettings *HTTPConfig `json:"httpSettings"`
|
||||
DSSettings *DomainSocketConfig `json:"dsSettings"`
|
||||
QUICSettings *QUICConfig `json:"quicSettings"`
|
||||
SocketSettings *SocketConfig `json:"sockopt"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
config := &internet.StreamConfig{
|
||||
ProtocolName: "tcp",
|
||||
}
|
||||
if c.Network != nil {
|
||||
protocol, err := c.Network.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.ProtocolName = protocol
|
||||
}
|
||||
if strings.EqualFold(c.Security, "tls") {
|
||||
tlsSettings := c.TLSSettings
|
||||
if tlsSettings == nil {
|
||||
if c.XTLSSettings != nil {
|
||||
return nil, newError(`TLS: Please use "tlsSettings" instead of "xtlsSettings".`)
|
||||
}
|
||||
tlsSettings = &TLSConfig{}
|
||||
}
|
||||
ts, err := tlsSettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build TLS config.").Base(err)
|
||||
}
|
||||
tm := serial.ToTypedMessage(ts)
|
||||
config.SecuritySettings = append(config.SecuritySettings, tm)
|
||||
config.SecurityType = tm.Type
|
||||
}
|
||||
if strings.EqualFold(c.Security, "xtls") {
|
||||
if config.ProtocolName != "tcp" && config.ProtocolName != "mkcp" && config.ProtocolName != "domainsocket" {
|
||||
return nil, newError("XTLS only supports TCP, mKCP and DomainSocket for now.")
|
||||
}
|
||||
xtlsSettings := c.XTLSSettings
|
||||
if xtlsSettings == nil {
|
||||
if c.TLSSettings != nil {
|
||||
return nil, newError(`XTLS: Please use "xtlsSettings" instead of "tlsSettings".`)
|
||||
}
|
||||
xtlsSettings = &XTLSConfig{}
|
||||
}
|
||||
ts, err := xtlsSettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build XTLS config.").Base(err)
|
||||
}
|
||||
tm := serial.ToTypedMessage(ts)
|
||||
config.SecuritySettings = append(config.SecuritySettings, tm)
|
||||
config.SecurityType = tm.Type
|
||||
}
|
||||
if c.TCPSettings != nil {
|
||||
ts, err := c.TCPSettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build TCP config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "tcp",
|
||||
Settings: serial.ToTypedMessage(ts),
|
||||
})
|
||||
}
|
||||
if c.KCPSettings != nil {
|
||||
ts, err := c.KCPSettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build mKCP config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "mkcp",
|
||||
Settings: serial.ToTypedMessage(ts),
|
||||
})
|
||||
}
|
||||
if c.WSSettings != nil {
|
||||
ts, err := c.WSSettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build WebSocket config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "websocket",
|
||||
Settings: serial.ToTypedMessage(ts),
|
||||
})
|
||||
}
|
||||
if c.HTTPSettings != nil {
|
||||
ts, err := c.HTTPSettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build HTTP config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "http",
|
||||
Settings: serial.ToTypedMessage(ts),
|
||||
})
|
||||
}
|
||||
if c.DSSettings != nil {
|
||||
ds, err := c.DSSettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build DomainSocket config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "domainsocket",
|
||||
Settings: serial.ToTypedMessage(ds),
|
||||
})
|
||||
}
|
||||
if c.QUICSettings != nil {
|
||||
qs, err := c.QUICSettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build QUIC config").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "quic",
|
||||
Settings: serial.ToTypedMessage(qs),
|
||||
})
|
||||
}
|
||||
if c.SocketSettings != nil {
|
||||
ss, err := c.SocketSettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("Failed to build sockopt").Base(err)
|
||||
}
|
||||
config.SocketSettings = ss
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type ProxyConfig struct {
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (v *ProxyConfig) Build() (*internet.ProxyConfig, error) {
|
||||
if v.Tag == "" {
|
||||
return nil, newError("Proxy tag is not set.")
|
||||
}
|
||||
return &internet.ProxyConfig{
|
||||
Tag: v.Tag,
|
||||
}, nil
|
||||
}
|
169
infra/conf/transport_test.go
Normal file
169
infra/conf/transport_test.go
Normal file
|
@ -0,0 +1,169 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/transport"
|
||||
"github.com/xtls/xray-core/v1/transport/internet"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/http"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/noop"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/headers/tls"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/kcp"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/quic"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/tcp"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/websocket"
|
||||
)
|
||||
|
||||
func TestSocketConfig(t *testing.T) {
|
||||
createParser := func() func(string) (proto.Message, error) {
|
||||
return func(s string) (proto.Message, error) {
|
||||
config := new(SocketConfig)
|
||||
if err := json.Unmarshal([]byte(s), config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config.Build()
|
||||
}
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"mark": 1,
|
||||
"tcpFastOpen": true
|
||||
}`,
|
||||
Parser: createParser(),
|
||||
Output: &internet.SocketConfig{
|
||||
Mark: 1,
|
||||
Tfo: internet.SocketConfig_Enable,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestTransportConfig(t *testing.T) {
|
||||
createParser := func() func(string) (proto.Message, error) {
|
||||
return func(s string) (proto.Message, error) {
|
||||
config := new(TransportConfig)
|
||||
if err := json.Unmarshal([]byte(s), config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config.Build()
|
||||
}
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"tcpSettings": {
|
||||
"header": {
|
||||
"type": "http",
|
||||
"request": {
|
||||
"version": "1.1",
|
||||
"method": "GET",
|
||||
"path": "/b",
|
||||
"headers": {
|
||||
"a": "b",
|
||||
"c": "d"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"version": "1.0",
|
||||
"status": "404",
|
||||
"reason": "Not Found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kcpSettings": {
|
||||
"mtu": 1200,
|
||||
"header": {
|
||||
"type": "none"
|
||||
}
|
||||
},
|
||||
"wsSettings": {
|
||||
"path": "/t"
|
||||
},
|
||||
"quicSettings": {
|
||||
"key": "abcd",
|
||||
"header": {
|
||||
"type": "dtls"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
Parser: createParser(),
|
||||
Output: &transport.Config{
|
||||
TransportSettings: []*internet.TransportConfig{
|
||||
{
|
||||
ProtocolName: "tcp",
|
||||
Settings: serial.ToTypedMessage(&tcp.Config{
|
||||
HeaderSettings: serial.ToTypedMessage(&http.Config{
|
||||
Request: &http.RequestConfig{
|
||||
Version: &http.Version{Value: "1.1"},
|
||||
Method: &http.Method{Value: "GET"},
|
||||
Uri: []string{"/b"},
|
||||
Header: []*http.Header{
|
||||
{Name: "a", Value: []string{"b"}},
|
||||
{Name: "c", Value: []string{"d"}},
|
||||
},
|
||||
},
|
||||
Response: &http.ResponseConfig{
|
||||
Version: &http.Version{Value: "1.0"},
|
||||
Status: &http.Status{Code: "404", Reason: "Not Found"},
|
||||
Header: []*http.Header{
|
||||
{
|
||||
Name: "Content-Type",
|
||||
Value: []string{"application/octet-stream", "video/mpeg"},
|
||||
},
|
||||
{
|
||||
Name: "Transfer-Encoding",
|
||||
Value: []string{"chunked"},
|
||||
},
|
||||
{
|
||||
Name: "Connection",
|
||||
Value: []string{"keep-alive"},
|
||||
},
|
||||
{
|
||||
Name: "Pragma",
|
||||
Value: []string{"no-cache"},
|
||||
},
|
||||
{
|
||||
Name: "Cache-Control",
|
||||
Value: []string{"private", "no-cache"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
ProtocolName: "mkcp",
|
||||
Settings: serial.ToTypedMessage(&kcp.Config{
|
||||
Mtu: &kcp.MTU{Value: 1200},
|
||||
HeaderConfig: serial.ToTypedMessage(&noop.Config{}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
ProtocolName: "websocket",
|
||||
Settings: serial.ToTypedMessage(&websocket.Config{
|
||||
Path: "/t",
|
||||
}),
|
||||
},
|
||||
{
|
||||
ProtocolName: "quic",
|
||||
Settings: serial.ToTypedMessage(&quic.Config{
|
||||
Key: "abcd",
|
||||
Security: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_NONE,
|
||||
},
|
||||
Header: serial.ToTypedMessage(&tls.PacketConfig{}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
175
infra/conf/trojan.go
Normal file
175
infra/conf/trojan.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/trojan"
|
||||
)
|
||||
|
||||
// TrojanServerTarget is configuration of a single trojan server
|
||||
type TrojanServerTarget struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Level byte `json:"level"`
|
||||
Flow string `json:"flow"`
|
||||
}
|
||||
|
||||
// TrojanClientConfig is configuration of trojan servers
|
||||
type TrojanClientConfig struct {
|
||||
Servers []*TrojanServerTarget `json:"servers"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *TrojanClientConfig) Build() (proto.Message, error) {
|
||||
config := new(trojan.ClientConfig)
|
||||
|
||||
if len(c.Servers) == 0 {
|
||||
return nil, newError("0 Trojan server configured.")
|
||||
}
|
||||
|
||||
serverSpecs := make([]*protocol.ServerEndpoint, len(c.Servers))
|
||||
for idx, rec := range c.Servers {
|
||||
if rec.Address == nil {
|
||||
return nil, newError("Trojan server address is not set.")
|
||||
}
|
||||
if rec.Port == 0 {
|
||||
return nil, newError("Invalid Trojan port.")
|
||||
}
|
||||
if rec.Password == "" {
|
||||
return nil, newError("Trojan password is not specified.")
|
||||
}
|
||||
account := &trojan.Account{
|
||||
Password: rec.Password,
|
||||
Flow: rec.Flow,
|
||||
}
|
||||
trojan := &protocol.ServerEndpoint{
|
||||
Address: rec.Address.Build(),
|
||||
Port: uint32(rec.Port),
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Level: uint32(rec.Level),
|
||||
Email: rec.Email,
|
||||
Account: serial.ToTypedMessage(account),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
serverSpecs[idx] = trojan
|
||||
}
|
||||
|
||||
config.Server = serverSpecs
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// TrojanInboundFallback is fallback configuration
|
||||
type TrojanInboundFallback struct {
|
||||
Alpn string `json:"alpn"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Dest json.RawMessage `json:"dest"`
|
||||
Xver uint64 `json:"xver"`
|
||||
}
|
||||
|
||||
// TrojanUserConfig is user configuration
|
||||
type TrojanUserConfig struct {
|
||||
Password string `json:"password"`
|
||||
Level byte `json:"level"`
|
||||
Email string `json:"email"`
|
||||
Flow string `json:"flow"`
|
||||
}
|
||||
|
||||
// TrojanServerConfig is Inbound configuration
|
||||
type TrojanServerConfig struct {
|
||||
Clients []*TrojanUserConfig `json:"clients"`
|
||||
Fallback json.RawMessage `json:"fallback"`
|
||||
Fallbacks []*TrojanInboundFallback `json:"fallbacks"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *TrojanServerConfig) Build() (proto.Message, error) {
|
||||
config := new(trojan.ServerConfig)
|
||||
config.Users = make([]*protocol.User, len(c.Clients))
|
||||
for idx, rawUser := range c.Clients {
|
||||
user := new(protocol.User)
|
||||
account := &trojan.Account{
|
||||
Password: rawUser.Password,
|
||||
Flow: rawUser.Flow,
|
||||
}
|
||||
|
||||
user.Email = rawUser.Email
|
||||
user.Level = uint32(rawUser.Level)
|
||||
user.Account = serial.ToTypedMessage(account)
|
||||
config.Users[idx] = user
|
||||
}
|
||||
|
||||
if c.Fallback != nil {
|
||||
return nil, newError(`Trojan settings: please use "fallbacks":[{}] instead of "fallback":{}`)
|
||||
}
|
||||
for _, fb := range c.Fallbacks {
|
||||
var i uint16
|
||||
var s string
|
||||
if err := json.Unmarshal(fb.Dest, &i); err == nil {
|
||||
s = strconv.Itoa(int(i))
|
||||
} else {
|
||||
_ = json.Unmarshal(fb.Dest, &s)
|
||||
}
|
||||
config.Fallbacks = append(config.Fallbacks, &trojan.Fallback{
|
||||
Alpn: fb.Alpn,
|
||||
Path: fb.Path,
|
||||
Type: fb.Type,
|
||||
Dest: s,
|
||||
Xver: fb.Xver,
|
||||
})
|
||||
}
|
||||
for _, fb := range config.Fallbacks {
|
||||
/*
|
||||
if fb.Alpn == "h2" && fb.Path != "" {
|
||||
return nil, newError(`Trojan fallbacks: "alpn":"h2" doesn't support "path"`)
|
||||
}
|
||||
*/
|
||||
if fb.Path != "" && fb.Path[0] != '/' {
|
||||
return nil, newError(`Trojan fallbacks: "path" must be empty or start with "/"`)
|
||||
}
|
||||
if fb.Type == "" && fb.Dest != "" {
|
||||
if fb.Dest == "serve-ws-none" {
|
||||
fb.Type = "serve"
|
||||
} else {
|
||||
switch fb.Dest[0] {
|
||||
case '@', '/':
|
||||
fb.Type = "unix"
|
||||
if fb.Dest[0] == '@' && len(fb.Dest) > 1 && fb.Dest[1] == '@' && runtime.GOOS == "linux" {
|
||||
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy
|
||||
copy(fullAddr, fb.Dest[1:])
|
||||
fb.Dest = string(fullAddr)
|
||||
}
|
||||
default:
|
||||
if _, err := strconv.Atoi(fb.Dest); err == nil {
|
||||
fb.Dest = "127.0.0.1:" + fb.Dest
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(fb.Dest); err == nil {
|
||||
fb.Type = "tcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if fb.Type == "" {
|
||||
return nil, newError(`Trojan fallbacks: please fill in a valid value for every "dest"`)
|
||||
}
|
||||
if fb.Xver > 2 {
|
||||
return nil, newError(`Trojan fallbacks: invalid PROXY protocol version, "xver" only accepts 0, 1, 2`)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
185
infra/conf/vless.go
Normal file
185
infra/conf/vless.go
Normal file
|
@ -0,0 +1,185 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/vless"
|
||||
"github.com/xtls/xray-core/v1/proxy/vless/inbound"
|
||||
"github.com/xtls/xray-core/v1/proxy/vless/outbound"
|
||||
)
|
||||
|
||||
type VLessInboundFallback struct {
|
||||
Alpn string `json:"alpn"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Dest json.RawMessage `json:"dest"`
|
||||
Xver uint64 `json:"xver"`
|
||||
}
|
||||
|
||||
type VLessInboundConfig struct {
|
||||
Clients []json.RawMessage `json:"clients"`
|
||||
Decryption string `json:"decryption"`
|
||||
Fallback json.RawMessage `json:"fallback"`
|
||||
Fallbacks []*VLessInboundFallback `json:"fallbacks"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *VLessInboundConfig) Build() (proto.Message, error) {
|
||||
config := new(inbound.Config)
|
||||
config.Clients = make([]*protocol.User, len(c.Clients))
|
||||
for idx, rawUser := range c.Clients {
|
||||
user := new(protocol.User)
|
||||
if err := json.Unmarshal(rawUser, user); err != nil {
|
||||
return nil, newError(`VLESS clients: invalid user`).Base(err)
|
||||
}
|
||||
account := new(vless.Account)
|
||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||
return nil, newError(`VLESS clients: invalid user`).Base(err)
|
||||
}
|
||||
|
||||
switch account.Flow {
|
||||
case "", "xtls-rprx-origin", "xtls-rprx-direct":
|
||||
default:
|
||||
return nil, newError(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`)
|
||||
}
|
||||
|
||||
if account.Encryption != "" {
|
||||
return nil, newError(`VLESS clients: "encryption" should not in inbound settings`)
|
||||
}
|
||||
|
||||
user.Account = serial.ToTypedMessage(account)
|
||||
config.Clients[idx] = user
|
||||
}
|
||||
|
||||
if c.Decryption != "none" {
|
||||
return nil, newError(`VLESS settings: please add/set "decryption":"none" to every settings`)
|
||||
}
|
||||
config.Decryption = c.Decryption
|
||||
|
||||
if c.Fallback != nil {
|
||||
return nil, newError(`VLESS settings: please use "fallbacks":[{}] instead of "fallback":{}`)
|
||||
}
|
||||
for _, fb := range c.Fallbacks {
|
||||
var i uint16
|
||||
var s string
|
||||
if err := json.Unmarshal(fb.Dest, &i); err == nil {
|
||||
s = strconv.Itoa(int(i))
|
||||
} else {
|
||||
_ = json.Unmarshal(fb.Dest, &s)
|
||||
}
|
||||
config.Fallbacks = append(config.Fallbacks, &inbound.Fallback{
|
||||
Alpn: fb.Alpn,
|
||||
Path: fb.Path,
|
||||
Type: fb.Type,
|
||||
Dest: s,
|
||||
Xver: fb.Xver,
|
||||
})
|
||||
}
|
||||
for _, fb := range config.Fallbacks {
|
||||
/*
|
||||
if fb.Alpn == "h2" && fb.Path != "" {
|
||||
return nil, newError(`VLESS fallbacks: "alpn":"h2" doesn't support "path"`)
|
||||
}
|
||||
*/
|
||||
if fb.Path != "" && fb.Path[0] != '/' {
|
||||
return nil, newError(`VLESS fallbacks: "path" must be empty or start with "/"`)
|
||||
}
|
||||
if fb.Type == "" && fb.Dest != "" {
|
||||
if fb.Dest == "serve-ws-none" {
|
||||
fb.Type = "serve"
|
||||
} else {
|
||||
switch fb.Dest[0] {
|
||||
case '@', '/':
|
||||
fb.Type = "unix"
|
||||
if fb.Dest[0] == '@' && len(fb.Dest) > 1 && fb.Dest[1] == '@' && runtime.GOOS == "linux" {
|
||||
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy
|
||||
copy(fullAddr, fb.Dest[1:])
|
||||
fb.Dest = string(fullAddr)
|
||||
}
|
||||
default:
|
||||
if _, err := strconv.Atoi(fb.Dest); err == nil {
|
||||
fb.Dest = "127.0.0.1:" + fb.Dest
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(fb.Dest); err == nil {
|
||||
fb.Type = "tcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if fb.Type == "" {
|
||||
return nil, newError(`VLESS fallbacks: please fill in a valid value for every "dest"`)
|
||||
}
|
||||
if fb.Xver > 2 {
|
||||
return nil, newError(`VLESS fallbacks: invalid PROXY protocol version, "xver" only accepts 0, 1, 2`)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type VLessOutboundVnext struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Users []json.RawMessage `json:"users"`
|
||||
}
|
||||
|
||||
type VLessOutboundConfig struct {
|
||||
Vnext []*VLessOutboundVnext `json:"vnext"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *VLessOutboundConfig) Build() (proto.Message, error) {
|
||||
config := new(outbound.Config)
|
||||
|
||||
if len(c.Vnext) == 0 {
|
||||
return nil, newError(`VLESS settings: "vnext" is empty`)
|
||||
}
|
||||
config.Vnext = make([]*protocol.ServerEndpoint, len(c.Vnext))
|
||||
for idx, rec := range c.Vnext {
|
||||
if rec.Address == nil {
|
||||
return nil, newError(`VLESS vnext: "address" is not set`)
|
||||
}
|
||||
if len(rec.Users) == 0 {
|
||||
return nil, newError(`VLESS vnext: "users" is empty`)
|
||||
}
|
||||
spec := &protocol.ServerEndpoint{
|
||||
Address: rec.Address.Build(),
|
||||
Port: uint32(rec.Port),
|
||||
User: make([]*protocol.User, len(rec.Users)),
|
||||
}
|
||||
for idx, rawUser := range rec.Users {
|
||||
user := new(protocol.User)
|
||||
if err := json.Unmarshal(rawUser, user); err != nil {
|
||||
return nil, newError(`VLESS users: invalid user`).Base(err)
|
||||
}
|
||||
account := new(vless.Account)
|
||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||
return nil, newError(`VLESS users: invalid user`).Base(err)
|
||||
}
|
||||
|
||||
switch account.Flow {
|
||||
case "", "xtls-rprx-origin", "xtls-rprx-origin-udp443", "xtls-rprx-direct", "xtls-rprx-direct-udp443":
|
||||
default:
|
||||
return nil, newError(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`)
|
||||
}
|
||||
|
||||
if account.Encryption != "none" {
|
||||
return nil, newError(`VLESS users: please add/set "encryption":"none" for every user`)
|
||||
}
|
||||
|
||||
user.Account = serial.ToTypedMessage(account)
|
||||
spec.User[idx] = user
|
||||
}
|
||||
config.Vnext[idx] = spec
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
134
infra/conf/vless_test.go
Normal file
134
infra/conf/vless_test.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/vless"
|
||||
"github.com/xtls/xray-core/v1/proxy/vless/inbound"
|
||||
"github.com/xtls/xray-core/v1/proxy/vless/outbound"
|
||||
)
|
||||
|
||||
func TestVLessOutbound(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(VLessOutboundConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"vnext": [{
|
||||
"address": "example.com",
|
||||
"port": 443,
|
||||
"users": [
|
||||
{
|
||||
"id": "27848739-7e62-4138-9fd3-098a63964b6b",
|
||||
"flow": "xtls-rprx-direct-udp443",
|
||||
"encryption": "none",
|
||||
"level": 0
|
||||
}
|
||||
]
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &outbound.Config{
|
||||
Vnext: []*protocol.ServerEndpoint{
|
||||
{
|
||||
Address: &net.IPOrDomain{
|
||||
Address: &net.IPOrDomain_Domain{
|
||||
Domain: "example.com",
|
||||
},
|
||||
},
|
||||
Port: 443,
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Account: serial.ToTypedMessage(&vless.Account{
|
||||
Id: "27848739-7e62-4138-9fd3-098a63964b6b",
|
||||
Flow: "xtls-rprx-direct-udp443",
|
||||
Encryption: "none",
|
||||
}),
|
||||
Level: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestVLessInbound(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(VLessInboundConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"clients": [
|
||||
{
|
||||
"id": "27848739-7e62-4138-9fd3-098a63964b6b",
|
||||
"flow": "xtls-rprx-direct",
|
||||
"level": 0,
|
||||
"email": "love@example.com"
|
||||
}
|
||||
],
|
||||
"decryption": "none",
|
||||
"fallbacks": [
|
||||
{
|
||||
"dest": 80
|
||||
},
|
||||
{
|
||||
"alpn": "h2",
|
||||
"dest": "@/dev/shm/domain.socket",
|
||||
"xver": 2
|
||||
},
|
||||
{
|
||||
"path": "/innerws",
|
||||
"dest": "serve-ws-none"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &inbound.Config{
|
||||
Clients: []*protocol.User{
|
||||
{
|
||||
Account: serial.ToTypedMessage(&vless.Account{
|
||||
Id: "27848739-7e62-4138-9fd3-098a63964b6b",
|
||||
Flow: "xtls-rprx-direct",
|
||||
}),
|
||||
Level: 0,
|
||||
Email: "love@example.com",
|
||||
},
|
||||
},
|
||||
Decryption: "none",
|
||||
Fallbacks: []*inbound.Fallback{
|
||||
{
|
||||
Alpn: "",
|
||||
Path: "",
|
||||
Type: "tcp",
|
||||
Dest: "127.0.0.1:80",
|
||||
Xver: 0,
|
||||
},
|
||||
{
|
||||
Alpn: "h2",
|
||||
Path: "",
|
||||
Type: "unix",
|
||||
Dest: "@/dev/shm/domain.socket",
|
||||
Xver: 2,
|
||||
},
|
||||
{
|
||||
Alpn: "",
|
||||
Path: "/innerws",
|
||||
Type: "serve",
|
||||
Dest: "serve-ws-none",
|
||||
Xver: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
159
infra/conf/vmess.go
Normal file
159
infra/conf/vmess.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess/inbound"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess/outbound"
|
||||
)
|
||||
|
||||
type VMessAccount struct {
|
||||
ID string `json:"id"`
|
||||
AlterIds uint16 `json:"alterId"`
|
||||
Security string `json:"security"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (a *VMessAccount) Build() *vmess.Account {
|
||||
var st protocol.SecurityType
|
||||
switch strings.ToLower(a.Security) {
|
||||
case "aes-128-gcm":
|
||||
st = protocol.SecurityType_AES128_GCM
|
||||
case "chacha20-poly1305":
|
||||
st = protocol.SecurityType_CHACHA20_POLY1305
|
||||
case "auto":
|
||||
st = protocol.SecurityType_AUTO
|
||||
case "none":
|
||||
st = protocol.SecurityType_NONE
|
||||
default:
|
||||
st = protocol.SecurityType_AUTO
|
||||
}
|
||||
return &vmess.Account{
|
||||
Id: a.ID,
|
||||
AlterId: uint32(a.AlterIds),
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: st,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type VMessDetourConfig struct {
|
||||
ToTag string `json:"to"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *VMessDetourConfig) Build() *inbound.DetourConfig {
|
||||
return &inbound.DetourConfig{
|
||||
To: c.ToTag,
|
||||
}
|
||||
}
|
||||
|
||||
type FeaturesConfig struct {
|
||||
Detour *VMessDetourConfig `json:"detour"`
|
||||
}
|
||||
|
||||
type VMessDefaultConfig struct {
|
||||
AlterIDs uint16 `json:"alterId"`
|
||||
Level byte `json:"level"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *VMessDefaultConfig) Build() *inbound.DefaultConfig {
|
||||
config := new(inbound.DefaultConfig)
|
||||
config.AlterId = uint32(c.AlterIDs)
|
||||
config.Level = uint32(c.Level)
|
||||
return config
|
||||
}
|
||||
|
||||
type VMessInboundConfig struct {
|
||||
Users []json.RawMessage `json:"clients"`
|
||||
Features *FeaturesConfig `json:"features"`
|
||||
Defaults *VMessDefaultConfig `json:"default"`
|
||||
DetourConfig *VMessDetourConfig `json:"detour"`
|
||||
SecureOnly bool `json:"disableInsecureEncryption"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *VMessInboundConfig) Build() (proto.Message, error) {
|
||||
config := &inbound.Config{
|
||||
SecureEncryptionOnly: c.SecureOnly,
|
||||
}
|
||||
|
||||
if c.Defaults != nil {
|
||||
config.Default = c.Defaults.Build()
|
||||
}
|
||||
|
||||
if c.DetourConfig != nil {
|
||||
config.Detour = c.DetourConfig.Build()
|
||||
} else if c.Features != nil && c.Features.Detour != nil {
|
||||
config.Detour = c.Features.Detour.Build()
|
||||
}
|
||||
|
||||
config.User = make([]*protocol.User, len(c.Users))
|
||||
for idx, rawData := range c.Users {
|
||||
user := new(protocol.User)
|
||||
if err := json.Unmarshal(rawData, user); err != nil {
|
||||
return nil, newError("invalid VMess user").Base(err)
|
||||
}
|
||||
account := new(VMessAccount)
|
||||
if err := json.Unmarshal(rawData, account); err != nil {
|
||||
return nil, newError("invalid VMess user").Base(err)
|
||||
}
|
||||
user.Account = serial.ToTypedMessage(account.Build())
|
||||
config.User[idx] = user
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type VMessOutboundTarget struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Users []json.RawMessage `json:"users"`
|
||||
}
|
||||
type VMessOutboundConfig struct {
|
||||
Receivers []*VMessOutboundTarget `json:"vnext"`
|
||||
}
|
||||
|
||||
// Build implements Buildable
|
||||
func (c *VMessOutboundConfig) Build() (proto.Message, error) {
|
||||
config := new(outbound.Config)
|
||||
|
||||
if len(c.Receivers) == 0 {
|
||||
return nil, newError("0 VMess receiver configured")
|
||||
}
|
||||
serverSpecs := make([]*protocol.ServerEndpoint, len(c.Receivers))
|
||||
for idx, rec := range c.Receivers {
|
||||
if len(rec.Users) == 0 {
|
||||
return nil, newError("0 user configured for VMess outbound")
|
||||
}
|
||||
if rec.Address == nil {
|
||||
return nil, newError("address is not set in VMess outbound config")
|
||||
}
|
||||
spec := &protocol.ServerEndpoint{
|
||||
Address: rec.Address.Build(),
|
||||
Port: uint32(rec.Port),
|
||||
}
|
||||
for _, rawUser := range rec.Users {
|
||||
user := new(protocol.User)
|
||||
if err := json.Unmarshal(rawUser, user); err != nil {
|
||||
return nil, newError("invalid VMess user").Base(err)
|
||||
}
|
||||
account := new(VMessAccount)
|
||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||
return nil, newError("invalid VMess user").Base(err)
|
||||
}
|
||||
user.Account = serial.ToTypedMessage(account.Build())
|
||||
spec.User = append(spec.User, user)
|
||||
}
|
||||
serverSpecs[idx] = spec
|
||||
}
|
||||
config.Receiver = serverSpecs
|
||||
return config, nil
|
||||
}
|
117
infra/conf/vmess_test.go
Normal file
117
infra/conf/vmess_test.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess/inbound"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess/outbound"
|
||||
)
|
||||
|
||||
func TestVMessOutbound(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(VMessOutboundConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"vnext": [{
|
||||
"address": "127.0.0.1",
|
||||
"port": 80,
|
||||
"users": [
|
||||
{
|
||||
"id": "e641f5ad-9397-41e3-bf1a-e8740dfed019",
|
||||
"email": "love@example.com",
|
||||
"level": 255
|
||||
}
|
||||
]
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &outbound.Config{
|
||||
Receiver: []*protocol.ServerEndpoint{
|
||||
{
|
||||
Address: &net.IPOrDomain{
|
||||
Address: &net.IPOrDomain_Ip{
|
||||
Ip: []byte{127, 0, 0, 1},
|
||||
},
|
||||
},
|
||||
Port: 80,
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Email: "love@example.com",
|
||||
Level: 255,
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019",
|
||||
AlterId: 0,
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_AUTO,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestVMessInbound(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(VMessInboundConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"clients": [
|
||||
{
|
||||
"id": "27848739-7e62-4138-9fd3-098a63964b6b",
|
||||
"level": 0,
|
||||
"alterId": 16,
|
||||
"email": "love@example.com",
|
||||
"security": "aes-128-gcm"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"level": 0,
|
||||
"alterId": 32
|
||||
},
|
||||
"detour": {
|
||||
"to": "tag_to_detour"
|
||||
},
|
||||
"disableInsecureEncryption": true
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &inbound.Config{
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Level: 0,
|
||||
Email: "love@example.com",
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: "27848739-7e62-4138-9fd3-098a63964b6b",
|
||||
AlterId: 16,
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_AES128_GCM,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Default: &inbound.DefaultConfig{
|
||||
Level: 0,
|
||||
AlterId: 32,
|
||||
},
|
||||
Detour: &inbound.DetourConfig{
|
||||
To: "tag_to_detour",
|
||||
},
|
||||
SecureEncryptionOnly: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
618
infra/conf/xray.go
Normal file
618
infra/conf/xray.go
Normal file
|
@ -0,0 +1,618 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/v1/app/dispatcher"
|
||||
"github.com/xtls/xray-core/v1/app/proxyman"
|
||||
"github.com/xtls/xray-core/v1/app/stats"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
core "github.com/xtls/xray-core/v1/core"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/xtls"
|
||||
)
|
||||
|
||||
var (
|
||||
inboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"dokodemo-door": func() interface{} { return new(DokodemoConfig) },
|
||||
"http": func() interface{} { return new(HTTPServerConfig) },
|
||||
"shadowsocks": func() interface{} { return new(ShadowsocksServerConfig) },
|
||||
"socks": func() interface{} { return new(SocksServerConfig) },
|
||||
"vless": func() interface{} { return new(VLessInboundConfig) },
|
||||
"vmess": func() interface{} { return new(VMessInboundConfig) },
|
||||
"trojan": func() interface{} { return new(TrojanServerConfig) },
|
||||
"mtproto": func() interface{} { return new(MTProtoServerConfig) },
|
||||
}, "protocol", "settings")
|
||||
|
||||
outboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"blackhole": func() interface{} { return new(BlackholeConfig) },
|
||||
"freedom": func() interface{} { return new(FreedomConfig) },
|
||||
"http": func() interface{} { return new(HTTPClientConfig) },
|
||||
"shadowsocks": func() interface{} { return new(ShadowsocksClientConfig) },
|
||||
"socks": func() interface{} { return new(SocksClientConfig) },
|
||||
"vless": func() interface{} { return new(VLessOutboundConfig) },
|
||||
"vmess": func() interface{} { return new(VMessOutboundConfig) },
|
||||
"trojan": func() interface{} { return new(TrojanClientConfig) },
|
||||
"mtproto": func() interface{} { return new(MTProtoClientConfig) },
|
||||
"dns": func() interface{} { return new(DNSOutboundConfig) },
|
||||
}, "protocol", "settings")
|
||||
|
||||
ctllog = log.New(os.Stderr, "xctl> ", 0)
|
||||
)
|
||||
|
||||
func toProtocolList(s []string) ([]proxyman.KnownProtocols, error) {
|
||||
kp := make([]proxyman.KnownProtocols, 0, 8)
|
||||
for _, p := range s {
|
||||
switch strings.ToLower(p) {
|
||||
case "http":
|
||||
kp = append(kp, proxyman.KnownProtocols_HTTP)
|
||||
case "https", "tls", "ssl":
|
||||
kp = append(kp, proxyman.KnownProtocols_TLS)
|
||||
default:
|
||||
return nil, newError("Unknown protocol: ", p)
|
||||
}
|
||||
}
|
||||
return kp, nil
|
||||
}
|
||||
|
||||
type SniffingConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DestOverride *StringList `json:"destOverride"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) {
|
||||
var p []string
|
||||
if c.DestOverride != nil {
|
||||
for _, domainOverride := range *c.DestOverride {
|
||||
switch strings.ToLower(domainOverride) {
|
||||
case "http":
|
||||
p = append(p, "http")
|
||||
case "tls", "https", "ssl":
|
||||
p = append(p, "tls")
|
||||
default:
|
||||
return nil, newError("unknown protocol: ", domainOverride)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &proxyman.SniffingConfig{
|
||||
Enabled: c.Enabled,
|
||||
DestinationOverride: p,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MuxConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Concurrency int16 `json:"concurrency"`
|
||||
}
|
||||
|
||||
// Build creates MultiplexingConfig, Concurrency < 0 completely disables mux.
|
||||
func (m *MuxConfig) Build() *proxyman.MultiplexingConfig {
|
||||
if m.Concurrency < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var con uint32 = 8
|
||||
if m.Concurrency > 0 {
|
||||
con = uint32(m.Concurrency)
|
||||
}
|
||||
|
||||
return &proxyman.MultiplexingConfig{
|
||||
Enabled: m.Enabled,
|
||||
Concurrency: con,
|
||||
}
|
||||
}
|
||||
|
||||
type InboundDetourAllocationConfig struct {
|
||||
Strategy string `json:"strategy"`
|
||||
Concurrency *uint32 `json:"concurrency"`
|
||||
RefreshMin *uint32 `json:"refresh"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *InboundDetourAllocationConfig) Build() (*proxyman.AllocationStrategy, error) {
|
||||
config := new(proxyman.AllocationStrategy)
|
||||
switch strings.ToLower(c.Strategy) {
|
||||
case "always":
|
||||
config.Type = proxyman.AllocationStrategy_Always
|
||||
case "random":
|
||||
config.Type = proxyman.AllocationStrategy_Random
|
||||
case "external":
|
||||
config.Type = proxyman.AllocationStrategy_External
|
||||
default:
|
||||
return nil, newError("unknown allocation strategy: ", c.Strategy)
|
||||
}
|
||||
if c.Concurrency != nil {
|
||||
config.Concurrency = &proxyman.AllocationStrategy_AllocationStrategyConcurrency{
|
||||
Value: *c.Concurrency,
|
||||
}
|
||||
}
|
||||
|
||||
if c.RefreshMin != nil {
|
||||
config.Refresh = &proxyman.AllocationStrategy_AllocationStrategyRefresh{
|
||||
Value: *c.RefreshMin,
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type InboundDetourConfig struct {
|
||||
Protocol string `json:"protocol"`
|
||||
PortRange *PortRange `json:"port"`
|
||||
ListenOn *Address `json:"listen"`
|
||||
Settings *json.RawMessage `json:"settings"`
|
||||
Tag string `json:"tag"`
|
||||
Allocation *InboundDetourAllocationConfig `json:"allocate"`
|
||||
StreamSetting *StreamConfig `json:"streamSettings"`
|
||||
DomainOverride *StringList `json:"domainOverride"`
|
||||
SniffingConfig *SniffingConfig `json:"sniffing"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
|
||||
receiverSettings := &proxyman.ReceiverConfig{}
|
||||
|
||||
if c.ListenOn == nil {
|
||||
// Listen on anyip, must set PortRange
|
||||
if c.PortRange == nil {
|
||||
return nil, newError("Listen on AnyIP but no Port(s) set in InboundDetour.")
|
||||
}
|
||||
receiverSettings.PortRange = c.PortRange.Build()
|
||||
} else {
|
||||
// Listen on specific IP or Unix Domain Socket
|
||||
receiverSettings.Listen = c.ListenOn.Build()
|
||||
listenDS := c.ListenOn.Family().IsDomain() && (c.ListenOn.Domain()[0] == '/' || c.ListenOn.Domain()[0] == '@')
|
||||
listenIP := c.ListenOn.Family().IsIP() || (c.ListenOn.Family().IsDomain() && c.ListenOn.Domain() == "localhost")
|
||||
if listenIP {
|
||||
// Listen on specific IP, must set PortRange
|
||||
if c.PortRange == nil {
|
||||
return nil, newError("Listen on specific ip without port in InboundDetour.")
|
||||
}
|
||||
// Listen on IP:Port
|
||||
receiverSettings.PortRange = c.PortRange.Build()
|
||||
} else if listenDS {
|
||||
if c.PortRange != nil {
|
||||
// Listen on Unix Domain Socket, PortRange should be nil
|
||||
receiverSettings.PortRange = nil
|
||||
}
|
||||
} else {
|
||||
return nil, newError("unable to listen on domain address: ", c.ListenOn.Domain())
|
||||
}
|
||||
}
|
||||
|
||||
if c.Allocation != nil {
|
||||
concurrency := -1
|
||||
if c.Allocation.Concurrency != nil && c.Allocation.Strategy == "random" {
|
||||
concurrency = int(*c.Allocation.Concurrency)
|
||||
}
|
||||
portRange := int(c.PortRange.To - c.PortRange.From + 1)
|
||||
if concurrency >= 0 && concurrency >= portRange {
|
||||
return nil, newError("not enough ports. concurrency = ", concurrency, " ports: ", c.PortRange.From, " - ", c.PortRange.To)
|
||||
}
|
||||
|
||||
as, err := c.Allocation.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receiverSettings.AllocationStrategy = as
|
||||
}
|
||||
if c.StreamSetting != nil {
|
||||
ss, err := c.StreamSetting.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ss.SecurityType == serial.GetMessageType(&xtls.Config{}) && !strings.EqualFold(c.Protocol, "vless") && !strings.EqualFold(c.Protocol, "trojan") {
|
||||
return nil, newError("XTLS doesn't supports " + c.Protocol + " for now.")
|
||||
}
|
||||
receiverSettings.StreamSettings = ss
|
||||
}
|
||||
if c.SniffingConfig != nil {
|
||||
s, err := c.SniffingConfig.Build()
|
||||
if err != nil {
|
||||
return nil, newError("failed to build sniffing config").Base(err)
|
||||
}
|
||||
receiverSettings.SniffingSettings = s
|
||||
}
|
||||
if c.DomainOverride != nil {
|
||||
kp, err := toProtocolList(*c.DomainOverride)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse inbound detour config").Base(err)
|
||||
}
|
||||
receiverSettings.DomainOverride = kp
|
||||
}
|
||||
|
||||
settings := []byte("{}")
|
||||
if c.Settings != nil {
|
||||
settings = ([]byte)(*c.Settings)
|
||||
}
|
||||
rawConfig, err := inboundConfigLoader.LoadWithID(settings, c.Protocol)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load inbound detour config.").Base(err)
|
||||
}
|
||||
if dokodemoConfig, ok := rawConfig.(*DokodemoConfig); ok {
|
||||
receiverSettings.ReceiveOriginalDestination = dokodemoConfig.Redirect
|
||||
}
|
||||
ts, err := rawConfig.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &core.InboundHandlerConfig{
|
||||
Tag: c.Tag,
|
||||
ReceiverSettings: serial.ToTypedMessage(receiverSettings),
|
||||
ProxySettings: serial.ToTypedMessage(ts),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type OutboundDetourConfig struct {
|
||||
Protocol string `json:"protocol"`
|
||||
SendThrough *Address `json:"sendThrough"`
|
||||
Tag string `json:"tag"`
|
||||
Settings *json.RawMessage `json:"settings"`
|
||||
StreamSetting *StreamConfig `json:"streamSettings"`
|
||||
ProxySettings *ProxyConfig `json:"proxySettings"`
|
||||
MuxSettings *MuxConfig `json:"mux"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
senderSettings := &proxyman.SenderConfig{}
|
||||
|
||||
if c.SendThrough != nil {
|
||||
address := c.SendThrough
|
||||
if address.Family().IsDomain() {
|
||||
return nil, newError("unable to send through: " + address.String())
|
||||
}
|
||||
senderSettings.Via = address.Build()
|
||||
}
|
||||
|
||||
if c.StreamSetting != nil {
|
||||
ss, err := c.StreamSetting.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ss.SecurityType == serial.GetMessageType(&xtls.Config{}) && !strings.EqualFold(c.Protocol, "vless") && !strings.EqualFold(c.Protocol, "trojan") {
|
||||
return nil, newError("XTLS doesn't supports " + c.Protocol + " for now.")
|
||||
}
|
||||
senderSettings.StreamSettings = ss
|
||||
}
|
||||
|
||||
if c.ProxySettings != nil {
|
||||
ps, err := c.ProxySettings.Build()
|
||||
if err != nil {
|
||||
return nil, newError("invalid outbound detour proxy settings.").Base(err)
|
||||
}
|
||||
senderSettings.ProxySettings = ps
|
||||
}
|
||||
|
||||
if c.MuxSettings != nil {
|
||||
ms := c.MuxSettings.Build()
|
||||
if ms != nil && ms.Enabled {
|
||||
if ss := senderSettings.StreamSettings; ss != nil {
|
||||
if ss.SecurityType == serial.GetMessageType(&xtls.Config{}) {
|
||||
return nil, newError("XTLS doesn't support Mux for now.")
|
||||
}
|
||||
}
|
||||
}
|
||||
senderSettings.MultiplexSettings = ms
|
||||
}
|
||||
|
||||
settings := []byte("{}")
|
||||
if c.Settings != nil {
|
||||
settings = ([]byte)(*c.Settings)
|
||||
}
|
||||
rawConfig, err := outboundConfigLoader.LoadWithID(settings, c.Protocol)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse to outbound detour config.").Base(err)
|
||||
}
|
||||
ts, err := rawConfig.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &core.OutboundHandlerConfig{
|
||||
SenderSettings: serial.ToTypedMessage(senderSettings),
|
||||
Tag: c.Tag,
|
||||
ProxySettings: serial.ToTypedMessage(ts),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type StatsConfig struct{}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *StatsConfig) Build() (*stats.Config, error) {
|
||||
return &stats.Config{}, nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// Port of this Point server.
|
||||
// Deprecated: Port exists for historical compatibility
|
||||
// and should not be used.
|
||||
Port uint16 `json:"port"`
|
||||
|
||||
// Deprecated: InboundConfig exists for historical compatibility
|
||||
// and should not be used.
|
||||
InboundConfig *InboundDetourConfig `json:"inbound"`
|
||||
|
||||
// Deprecated: OutboundConfig exists for historical compatibility
|
||||
// and should not be used.
|
||||
OutboundConfig *OutboundDetourConfig `json:"outbound"`
|
||||
|
||||
// Deprecated: InboundDetours exists for historical compatibility
|
||||
// and should not be used.
|
||||
InboundDetours []InboundDetourConfig `json:"inboundDetour"`
|
||||
|
||||
// Deprecated: OutboundDetours exists for historical compatibility
|
||||
// and should not be used.
|
||||
OutboundDetours []OutboundDetourConfig `json:"outboundDetour"`
|
||||
|
||||
LogConfig *LogConfig `json:"log"`
|
||||
RouterConfig *RouterConfig `json:"routing"`
|
||||
DNSConfig *DNSConfig `json:"dns"`
|
||||
InboundConfigs []InboundDetourConfig `json:"inbounds"`
|
||||
OutboundConfigs []OutboundDetourConfig `json:"outbounds"`
|
||||
Transport *TransportConfig `json:"transport"`
|
||||
Policy *PolicyConfig `json:"policy"`
|
||||
API *APIConfig `json:"api"`
|
||||
Stats *StatsConfig `json:"stats"`
|
||||
Reverse *ReverseConfig `json:"reverse"`
|
||||
}
|
||||
|
||||
func (c *Config) findInboundTag(tag string) int {
|
||||
found := -1
|
||||
for idx, ib := range c.InboundConfigs {
|
||||
if ib.Tag == tag {
|
||||
found = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func (c *Config) findOutboundTag(tag string) int {
|
||||
found := -1
|
||||
for idx, ob := range c.OutboundConfigs {
|
||||
if ob.Tag == tag {
|
||||
found = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// Override method accepts another Config overrides the current attribute
|
||||
func (c *Config) Override(o *Config, fn string) {
|
||||
// only process the non-deprecated members
|
||||
|
||||
if o.LogConfig != nil {
|
||||
c.LogConfig = o.LogConfig
|
||||
}
|
||||
if o.RouterConfig != nil {
|
||||
c.RouterConfig = o.RouterConfig
|
||||
}
|
||||
if o.DNSConfig != nil {
|
||||
c.DNSConfig = o.DNSConfig
|
||||
}
|
||||
if o.Transport != nil {
|
||||
c.Transport = o.Transport
|
||||
}
|
||||
if o.Policy != nil {
|
||||
c.Policy = o.Policy
|
||||
}
|
||||
if o.API != nil {
|
||||
c.API = o.API
|
||||
}
|
||||
if o.Stats != nil {
|
||||
c.Stats = o.Stats
|
||||
}
|
||||
if o.Reverse != nil {
|
||||
c.Reverse = o.Reverse
|
||||
}
|
||||
|
||||
// deprecated attrs... keep them for now
|
||||
if o.InboundConfig != nil {
|
||||
c.InboundConfig = o.InboundConfig
|
||||
}
|
||||
if o.OutboundConfig != nil {
|
||||
c.OutboundConfig = o.OutboundConfig
|
||||
}
|
||||
if o.InboundDetours != nil {
|
||||
c.InboundDetours = o.InboundDetours
|
||||
}
|
||||
if o.OutboundDetours != nil {
|
||||
c.OutboundDetours = o.OutboundDetours
|
||||
}
|
||||
// deprecated attrs
|
||||
|
||||
// update the Inbound in slice if the only one in overide config has same tag
|
||||
if len(o.InboundConfigs) > 0 {
|
||||
if len(c.InboundConfigs) > 0 && len(o.InboundConfigs) == 1 {
|
||||
if idx := c.findInboundTag(o.InboundConfigs[0].Tag); idx > -1 {
|
||||
c.InboundConfigs[idx] = o.InboundConfigs[0]
|
||||
ctllog.Println("[", fn, "] updated inbound with tag: ", o.InboundConfigs[0].Tag)
|
||||
} else {
|
||||
c.InboundConfigs = append(c.InboundConfigs, o.InboundConfigs[0])
|
||||
ctllog.Println("[", fn, "] appended inbound with tag: ", o.InboundConfigs[0].Tag)
|
||||
}
|
||||
} else {
|
||||
c.InboundConfigs = o.InboundConfigs
|
||||
}
|
||||
}
|
||||
|
||||
// update the Outbound in slice if the only one in overide config has same tag
|
||||
if len(o.OutboundConfigs) > 0 {
|
||||
if len(c.OutboundConfigs) > 0 && len(o.OutboundConfigs) == 1 {
|
||||
if idx := c.findOutboundTag(o.OutboundConfigs[0].Tag); idx > -1 {
|
||||
c.OutboundConfigs[idx] = o.OutboundConfigs[0]
|
||||
ctllog.Println("[", fn, "] updated outbound with tag: ", o.OutboundConfigs[0].Tag)
|
||||
} else {
|
||||
if strings.Contains(strings.ToLower(fn), "tail") {
|
||||
c.OutboundConfigs = append(c.OutboundConfigs, o.OutboundConfigs[0])
|
||||
ctllog.Println("[", fn, "] appended outbound with tag: ", o.OutboundConfigs[0].Tag)
|
||||
} else {
|
||||
c.OutboundConfigs = append(o.OutboundConfigs, c.OutboundConfigs...)
|
||||
ctllog.Println("[", fn, "] prepended outbound with tag: ", o.OutboundConfigs[0].Tag)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.OutboundConfigs = o.OutboundConfigs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyTransportConfig(s *StreamConfig, t *TransportConfig) {
|
||||
if s.TCPSettings == nil {
|
||||
s.TCPSettings = t.TCPConfig
|
||||
}
|
||||
if s.KCPSettings == nil {
|
||||
s.KCPSettings = t.KCPConfig
|
||||
}
|
||||
if s.WSSettings == nil {
|
||||
s.WSSettings = t.WSConfig
|
||||
}
|
||||
if s.HTTPSettings == nil {
|
||||
s.HTTPSettings = t.HTTPConfig
|
||||
}
|
||||
if s.DSSettings == nil {
|
||||
s.DSSettings = t.DSConfig
|
||||
}
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *Config) Build() (*core.Config, error) {
|
||||
config := &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&dispatcher.Config{}),
|
||||
serial.ToTypedMessage(&proxyman.InboundConfig{}),
|
||||
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
|
||||
},
|
||||
}
|
||||
|
||||
if c.API != nil {
|
||||
apiConf, err := c.API.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.App = append(config.App, serial.ToTypedMessage(apiConf))
|
||||
}
|
||||
|
||||
if c.Stats != nil {
|
||||
statsConf, err := c.Stats.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.App = append(config.App, serial.ToTypedMessage(statsConf))
|
||||
}
|
||||
|
||||
var logConfMsg *serial.TypedMessage
|
||||
if c.LogConfig != nil {
|
||||
logConfMsg = serial.ToTypedMessage(c.LogConfig.Build())
|
||||
} else {
|
||||
logConfMsg = serial.ToTypedMessage(DefaultLogConfig())
|
||||
}
|
||||
// let logger module be the first App to start,
|
||||
// so that other modules could print log during initiating
|
||||
config.App = append([]*serial.TypedMessage{logConfMsg}, config.App...)
|
||||
|
||||
if c.RouterConfig != nil {
|
||||
routerConfig, err := c.RouterConfig.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.App = append(config.App, serial.ToTypedMessage(routerConfig))
|
||||
}
|
||||
|
||||
if c.DNSConfig != nil {
|
||||
dnsApp, err := c.DNSConfig.Build()
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse DNS config").Base(err)
|
||||
}
|
||||
config.App = append(config.App, serial.ToTypedMessage(dnsApp))
|
||||
}
|
||||
|
||||
if c.Policy != nil {
|
||||
pc, err := c.Policy.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.App = append(config.App, serial.ToTypedMessage(pc))
|
||||
}
|
||||
|
||||
if c.Reverse != nil {
|
||||
r, err := c.Reverse.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.App = append(config.App, serial.ToTypedMessage(r))
|
||||
}
|
||||
|
||||
var inbounds []InboundDetourConfig
|
||||
|
||||
if c.InboundConfig != nil {
|
||||
inbounds = append(inbounds, *c.InboundConfig)
|
||||
}
|
||||
|
||||
if len(c.InboundDetours) > 0 {
|
||||
inbounds = append(inbounds, c.InboundDetours...)
|
||||
}
|
||||
|
||||
if len(c.InboundConfigs) > 0 {
|
||||
inbounds = append(inbounds, c.InboundConfigs...)
|
||||
}
|
||||
|
||||
// Backward compatibility.
|
||||
if len(inbounds) > 0 && inbounds[0].PortRange == nil && c.Port > 0 {
|
||||
inbounds[0].PortRange = &PortRange{
|
||||
From: uint32(c.Port),
|
||||
To: uint32(c.Port),
|
||||
}
|
||||
}
|
||||
|
||||
for _, rawInboundConfig := range inbounds {
|
||||
if c.Transport != nil {
|
||||
if rawInboundConfig.StreamSetting == nil {
|
||||
rawInboundConfig.StreamSetting = &StreamConfig{}
|
||||
}
|
||||
applyTransportConfig(rawInboundConfig.StreamSetting, c.Transport)
|
||||
}
|
||||
ic, err := rawInboundConfig.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Inbound = append(config.Inbound, ic)
|
||||
}
|
||||
|
||||
var outbounds []OutboundDetourConfig
|
||||
|
||||
if c.OutboundConfig != nil {
|
||||
outbounds = append(outbounds, *c.OutboundConfig)
|
||||
}
|
||||
|
||||
if len(c.OutboundDetours) > 0 {
|
||||
outbounds = append(outbounds, c.OutboundDetours...)
|
||||
}
|
||||
|
||||
if len(c.OutboundConfigs) > 0 {
|
||||
outbounds = append(outbounds, c.OutboundConfigs...)
|
||||
}
|
||||
|
||||
for _, rawOutboundConfig := range outbounds {
|
||||
if c.Transport != nil {
|
||||
if rawOutboundConfig.StreamSetting == nil {
|
||||
rawOutboundConfig.StreamSetting = &StreamConfig{}
|
||||
}
|
||||
applyTransportConfig(rawOutboundConfig.StreamSetting, c.Transport)
|
||||
}
|
||||
oc, err := rawOutboundConfig.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Outbound = append(config.Outbound, oc)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
449
infra/conf/xray_test.go
Normal file
449
infra/conf/xray_test.go
Normal file
|
@ -0,0 +1,449 @@
|
|||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/xtls/xray-core/v1/app/dispatcher"
|
||||
"github.com/xtls/xray-core/v1/app/log"
|
||||
"github.com/xtls/xray-core/v1/app/proxyman"
|
||||
"github.com/xtls/xray-core/v1/app/router"
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
clog "github.com/xtls/xray-core/v1/common/log"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
core "github.com/xtls/xray-core/v1/core"
|
||||
. "github.com/xtls/xray-core/v1/infra/conf"
|
||||
"github.com/xtls/xray-core/v1/proxy/blackhole"
|
||||
dns_proxy "github.com/xtls/xray-core/v1/proxy/dns"
|
||||
"github.com/xtls/xray-core/v1/proxy/freedom"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess/inbound"
|
||||
"github.com/xtls/xray-core/v1/transport/internet"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/http"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/tls"
|
||||
"github.com/xtls/xray-core/v1/transport/internet/websocket"
|
||||
)
|
||||
|
||||
func TestXrayConfig(t *testing.T) {
|
||||
createParser := func() func(string) (proto.Message, error) {
|
||||
return func(s string) (proto.Message, error) {
|
||||
config := new(Config)
|
||||
if err := json.Unmarshal([]byte(s), config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config.Build()
|
||||
}
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"outbound": {
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
},
|
||||
"log": {
|
||||
"access": "/var/log/xray/access.log",
|
||||
"loglevel": "error",
|
||||
"error": "/var/log/xray/error.log"
|
||||
},
|
||||
"inbound": {
|
||||
"streamSettings": {
|
||||
"network": "ws",
|
||||
"wsSettings": {
|
||||
"headers": {
|
||||
"host": "example.domain"
|
||||
},
|
||||
"path": ""
|
||||
},
|
||||
"tlsSettings": {
|
||||
"alpn": "h2"
|
||||
},
|
||||
"security": "tls"
|
||||
},
|
||||
"protocol": "vmess",
|
||||
"port": 443,
|
||||
"settings": {
|
||||
"clients": [
|
||||
{
|
||||
"alterId": 100,
|
||||
"security": "aes-128-gcm",
|
||||
"id": "0cdf8a45-303d-4fed-9780-29aa7f54175e"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"inbounds": [{
|
||||
"streamSettings": {
|
||||
"network": "ws",
|
||||
"wsSettings": {
|
||||
"headers": {
|
||||
"host": "example.domain"
|
||||
},
|
||||
"path": ""
|
||||
},
|
||||
"tlsSettings": {
|
||||
"alpn": "h2"
|
||||
},
|
||||
"security": "tls"
|
||||
},
|
||||
"protocol": "vmess",
|
||||
"port": "443-500",
|
||||
"allocate": {
|
||||
"strategy": "random",
|
||||
"concurrency": 3
|
||||
},
|
||||
"settings": {
|
||||
"clients": [
|
||||
{
|
||||
"alterId": 100,
|
||||
"security": "aes-128-gcm",
|
||||
"id": "0cdf8a45-303d-4fed-9780-29aa7f54175e"
|
||||
}
|
||||
]
|
||||
}
|
||||
}],
|
||||
"outboundDetour": [
|
||||
{
|
||||
"tag": "blocked",
|
||||
"protocol": "blackhole"
|
||||
},
|
||||
{
|
||||
"protocol": "dns"
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"strategy": "rules",
|
||||
"settings": {
|
||||
"rules": [
|
||||
{
|
||||
"ip": [
|
||||
"10.0.0.0/8"
|
||||
],
|
||||
"type": "field",
|
||||
"outboundTag": "blocked"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"transport": {
|
||||
"httpSettings": {
|
||||
"path": "/test"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
Parser: createParser(),
|
||||
Output: &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&log.Config{
|
||||
ErrorLogType: log.LogType_File,
|
||||
ErrorLogPath: "/var/log/xray/error.log",
|
||||
ErrorLogLevel: clog.Severity_Error,
|
||||
AccessLogType: log.LogType_File,
|
||||
AccessLogPath: "/var/log/xray/access.log",
|
||||
}),
|
||||
serial.ToTypedMessage(&dispatcher.Config{}),
|
||||
serial.ToTypedMessage(&proxyman.InboundConfig{}),
|
||||
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
|
||||
serial.ToTypedMessage(&router.Config{
|
||||
DomainStrategy: router.Config_AsIs,
|
||||
Rule: []*router.RoutingRule{
|
||||
{
|
||||
Geoip: []*router.GeoIP{
|
||||
{
|
||||
Cidr: []*router.CIDR{
|
||||
{
|
||||
Ip: []byte{10, 0, 0, 0},
|
||||
Prefix: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: "blocked",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "tcp",
|
||||
TransportSettings: []*internet.TransportConfig{
|
||||
{
|
||||
ProtocolName: "http",
|
||||
Settings: serial.ToTypedMessage(&http.Config{
|
||||
Path: "/test",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
DomainStrategy: freedom.Config_AS_IS,
|
||||
UserLevel: 0,
|
||||
}),
|
||||
},
|
||||
{
|
||||
Tag: "blocked",
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "tcp",
|
||||
TransportSettings: []*internet.TransportConfig{
|
||||
{
|
||||
ProtocolName: "http",
|
||||
Settings: serial.ToTypedMessage(&http.Config{
|
||||
Path: "/test",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&blackhole.Config{}),
|
||||
},
|
||||
{
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "tcp",
|
||||
TransportSettings: []*internet.TransportConfig{
|
||||
{
|
||||
ProtocolName: "http",
|
||||
Settings: serial.ToTypedMessage(&http.Config{
|
||||
Path: "/test",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{
|
||||
Server: &net.Endpoint{},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortRange: &net.PortRange{
|
||||
From: 443,
|
||||
To: 443,
|
||||
},
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "websocket",
|
||||
TransportSettings: []*internet.TransportConfig{
|
||||
{
|
||||
ProtocolName: "websocket",
|
||||
Settings: serial.ToTypedMessage(&websocket.Config{
|
||||
Header: []*websocket.Header{
|
||||
{
|
||||
Key: "host",
|
||||
Value: "example.domain",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
ProtocolName: "http",
|
||||
Settings: serial.ToTypedMessage(&http.Config{
|
||||
Path: "/test",
|
||||
}),
|
||||
},
|
||||
},
|
||||
SecurityType: "xray.transport.internet.tls.Config",
|
||||
SecuritySettings: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&tls.Config{
|
||||
NextProtocol: []string{"h2"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&inbound.Config{
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Level: 0,
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: "0cdf8a45-303d-4fed-9780-29aa7f54175e",
|
||||
AlterId: 100,
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_AES128_GCM,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortRange: &net.PortRange{
|
||||
From: 443,
|
||||
To: 500,
|
||||
},
|
||||
AllocationStrategy: &proxyman.AllocationStrategy{
|
||||
Type: proxyman.AllocationStrategy_Random,
|
||||
Concurrency: &proxyman.AllocationStrategy_AllocationStrategyConcurrency{
|
||||
Value: 3,
|
||||
},
|
||||
},
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "websocket",
|
||||
TransportSettings: []*internet.TransportConfig{
|
||||
{
|
||||
ProtocolName: "websocket",
|
||||
Settings: serial.ToTypedMessage(&websocket.Config{
|
||||
Header: []*websocket.Header{
|
||||
{
|
||||
Key: "host",
|
||||
Value: "example.domain",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
ProtocolName: "http",
|
||||
Settings: serial.ToTypedMessage(&http.Config{
|
||||
Path: "/test",
|
||||
}),
|
||||
},
|
||||
},
|
||||
SecurityType: "xray.transport.internet.tls.Config",
|
||||
SecuritySettings: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&tls.Config{
|
||||
NextProtocol: []string{"h2"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&inbound.Config{
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Level: 0,
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: "0cdf8a45-303d-4fed-9780-29aa7f54175e",
|
||||
AlterId: 100,
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_AES128_GCM,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMuxConfig_Build(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fields string
|
||||
want *proxyman.MultiplexingConfig
|
||||
}{
|
||||
{"default", `{"enabled": true, "concurrency": 16}`, &proxyman.MultiplexingConfig{
|
||||
Enabled: true,
|
||||
Concurrency: 16,
|
||||
}},
|
||||
{"empty def", `{}`, &proxyman.MultiplexingConfig{
|
||||
Enabled: false,
|
||||
Concurrency: 8,
|
||||
}},
|
||||
{"not enable", `{"enabled": false, "concurrency": 4}`, &proxyman.MultiplexingConfig{
|
||||
Enabled: false,
|
||||
Concurrency: 4,
|
||||
}},
|
||||
{"forbidden", `{"enabled": false, "concurrency": -1}`, nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &MuxConfig{}
|
||||
common.Must(json.Unmarshal([]byte(tt.fields), m))
|
||||
if got := m.Build(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("MuxConfig.Build() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Override(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
orig *Config
|
||||
over *Config
|
||||
fn string
|
||||
want *Config
|
||||
}{
|
||||
{"combine/empty",
|
||||
&Config{},
|
||||
&Config{
|
||||
LogConfig: &LogConfig{},
|
||||
RouterConfig: &RouterConfig{},
|
||||
DNSConfig: &DNSConfig{},
|
||||
Transport: &TransportConfig{},
|
||||
Policy: &PolicyConfig{},
|
||||
API: &APIConfig{},
|
||||
Stats: &StatsConfig{},
|
||||
Reverse: &ReverseConfig{},
|
||||
},
|
||||
"",
|
||||
&Config{
|
||||
LogConfig: &LogConfig{},
|
||||
RouterConfig: &RouterConfig{},
|
||||
DNSConfig: &DNSConfig{},
|
||||
Transport: &TransportConfig{},
|
||||
Policy: &PolicyConfig{},
|
||||
API: &APIConfig{},
|
||||
Stats: &StatsConfig{},
|
||||
Reverse: &ReverseConfig{},
|
||||
},
|
||||
},
|
||||
{"combine/newattr",
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{Tag: "old"}}},
|
||||
&Config{LogConfig: &LogConfig{}}, "",
|
||||
&Config{LogConfig: &LogConfig{}, InboundConfigs: []InboundDetourConfig{{Tag: "old"}}}},
|
||||
{"replace/inbounds",
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}},
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}}},
|
||||
"",
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Tag: "pos1", Protocol: "kcp"}}}},
|
||||
{"replace/inbounds-replaceall",
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}},
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}},
|
||||
"",
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}}},
|
||||
{"replace/notag-append",
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{}, {Protocol: "vmess"}}},
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}}},
|
||||
"",
|
||||
&Config{InboundConfigs: []InboundDetourConfig{{}, {Protocol: "vmess"}, {Tag: "pos1", Protocol: "kcp"}}}},
|
||||
{"replace/outbounds",
|
||||
&Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}},
|
||||
&Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}}},
|
||||
"",
|
||||
&Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Tag: "pos1", Protocol: "kcp"}}}},
|
||||
{"replace/outbounds-prepend",
|
||||
&Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}},
|
||||
&Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}},
|
||||
"config.json",
|
||||
&Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}}},
|
||||
{"replace/outbounds-append",
|
||||
&Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}},
|
||||
&Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos2", Protocol: "kcp"}}},
|
||||
"config_tail.json",
|
||||
&Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}, {Tag: "pos2", Protocol: "kcp"}}}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.orig.Override(tt.over, tt.fn)
|
||||
if r := cmp.Diff(tt.orig, tt.want); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
86
infra/vprotogen/main.go
Normal file
86
infra/vprotogen/main.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/core"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pwd, wdErr := os.Getwd()
|
||||
if wdErr != nil {
|
||||
fmt.Println("Can not get current working directory.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
GOBIN := common.GetGOBIN()
|
||||
binPath := os.Getenv("PATH")
|
||||
pathSlice := []string{binPath, GOBIN, pwd}
|
||||
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
|
||||
os.Setenv("PATH", binPath)
|
||||
|
||||
EXE := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
EXE = ".exe"
|
||||
}
|
||||
protoc := "protoc" + EXE
|
||||
|
||||
if path, err := exec.LookPath(protoc); err != nil {
|
||||
fmt.Println("Make sure that you have `" + protoc + "` in your system path or current path. To download it, please visit https://github.com/protocolbuffers/protobuf/releases")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
protoc = path
|
||||
}
|
||||
|
||||
protoFilesMap := make(map[string][]string)
|
||||
walkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
filename := filepath.Base(path)
|
||||
if strings.HasSuffix(filename, ".proto") {
|
||||
protoFilesMap[dir] = append(protoFilesMap[dir], path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if walkErr != nil {
|
||||
fmt.Println(walkErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, files := range protoFilesMap {
|
||||
for _, relProtoFile := range files {
|
||||
var args []string
|
||||
if core.ProtoFilesUsingProtocGenGoFast[relProtoFile] {
|
||||
args = []string{"--gofast_out", pwd, "--gofast_opt", "paths=source_relative", "--plugin", "protoc-gen-gofast=" + GOBIN + "/protoc-gen-gofast" + EXE}
|
||||
} else {
|
||||
args = []string{"--go_out", pwd, "--go_opt", "paths=source_relative", "--go-grpc_out", pwd, "--go-grpc_opt", "paths=source_relative", "--plugin", "protoc-gen-go=" + GOBIN + "/protoc-gen-go" + EXE, "--plugin", "protoc-gen-go-grpc=" + GOBIN + "/protoc-gen-go-grpc" + EXE}
|
||||
}
|
||||
args = append(args, relProtoFile)
|
||||
cmd := exec.Command(protoc, args...)
|
||||
cmd.Env = append(cmd.Env, os.Environ()...)
|
||||
output, cmdErr := cmd.CombinedOutput()
|
||||
if len(output) > 0 {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
if cmdErr != nil {
|
||||
fmt.Println(cmdErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue