feat: support export config outline and config marshal (#27)

This commit is contained in:
mzz 2023-02-25 22:53:18 +08:00 committed by GitHub
parent 90dac980b9
commit 5cf6dca509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 689 additions and 153 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
bpf_bpfeb*.go
bpf_bpfel*.go
dae
outline.json

View File

@ -25,4 +25,5 @@ func Execute() error {
func init() {
rootCmd.AddCommand(runCmd)
rootCmd.AddCommand(validateCmd)
rootCmd.AddCommand(exportCmd)
}

31
cmd/export.go Normal file
View File

@ -0,0 +1,31 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, v2rayA Organization <team@v2raya.org>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/v2rayA/dae/config"
)
var (
exportCmd = &cobra.Command{
Use: "export",
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Help()
},
}
exportOutlineCmd = &cobra.Command{
Use: "outline",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(config.ExportOutlineJson(Version))
},
}
)
func init() {
exportCmd.AddCommand(exportOutlineCmd)
}

View File

@ -32,16 +32,16 @@ var (
internal.AutoSu()
// Read config from --config cfgFile.
param, includes, err := readConfig(cfgFile)
conf, includes, err := readConfig(cfgFile)
if err != nil {
logrus.Fatalln("readConfig:", err)
}
log := logger.NewLogger(param.Global.LogLevel, disableTimestamp)
log := logger.NewLogger(conf.Global.LogLevel, disableTimestamp)
logrus.SetLevel(log.Level)
log.Infof("Include config files: [%v]", strings.Join(includes, ", "))
if err := Run(log, param); err != nil {
if err := Run(log, conf); err != nil {
logrus.Fatalln(err)
}
},
@ -53,16 +53,18 @@ func init() {
runCmd.PersistentFlags().BoolVarP(&disableTimestamp, "disable-timestamp", "", false, "disable timestamp")
}
func Run(log *logrus.Logger, param *config.Params) (err error) {
func Run(log *logrus.Logger, conf *config.Config) (err error) {
/// Get tag -> nodeList mapping.
tagToNodeList := map[string][]string{}
if len(param.Node) > 0 {
tagToNodeList[""] = append(tagToNodeList[""], param.Node...)
if len(conf.Node) > 0 {
for _, node := range conf.Node {
tagToNodeList[""] = append(tagToNodeList[""], string(node))
}
}
// Resolve subscriptions to nodes.
for _, sub := range param.Subscription {
tag, nodes, err := internal.ResolveSubscription(log, filepath.Dir(cfgFile), sub)
for _, sub := range conf.Subscription {
tag, nodes, err := internal.ResolveSubscription(log, filepath.Dir(cfgFile), string(sub))
if err != nil {
log.Warnf(`failed to resolve subscription "%v": %v`, sub, err)
}
@ -74,7 +76,7 @@ func Run(log *logrus.Logger, param *config.Params) (err error) {
return fmt.Errorf("no node found, which could because all subscription resolving failed")
}
if len(param.Global.LanInterface) == 0 && len(param.Global.WanInterface) == 0 {
if len(conf.Global.LanInterface) == 0 && len(conf.Global.WanInterface) == 0 {
return fmt.Errorf("LanInterface and WanInterface cannot both be empty")
}
@ -82,10 +84,10 @@ func Run(log *logrus.Logger, param *config.Params) (err error) {
t, err := control.NewControlPlane(
log,
tagToNodeList,
param.Group,
&param.Routing,
&param.Global,
&param.Dns,
conf.Group,
&conf.Routing,
&conf.Global,
&conf.Dns,
)
if err != nil {
return err
@ -98,7 +100,7 @@ func Run(log *logrus.Logger, param *config.Params) (err error) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGILL)
go func() {
if err := t.ListenAndServe(param.Global.TproxyPort); err != nil {
if err := t.ListenAndServe(conf.Global.TproxyPort); err != nil {
log.Errorln("ListenAndServe:", err)
sigs <- nil
}
@ -110,14 +112,14 @@ func Run(log *logrus.Logger, param *config.Params) (err error) {
return nil
}
func readConfig(cfgFile string) (params *config.Params, includes []string, err error) {
func readConfig(cfgFile string) (conf *config.Config, includes []string, err error) {
merger := config.NewMerger(cfgFile)
sections, includes, err := merger.Merge()
if err != nil {
return nil, nil, err
}
if params, err = config.New(sections); err != nil {
if conf, err = config.New(sections); err != nil {
return nil, nil, err
}
return params, includes, nil
return conf, includes, nil
}

View File

@ -46,7 +46,7 @@ func New(log *logrus.Logger, dns *config.Dns, opt *NewOption) (s *Dns, err error
return nil, fmt.Errorf("too many upstreams")
}
tag, link := common.GetTagFromLinkLikePlaintext(upstreamRaw)
tag, link := common.GetTagFromLinkLikePlaintext(string(upstreamRaw))
if tag == "" {
return nil, fmt.Errorf("%w: '%v' has no tag", BadUpstreamFormatError, upstreamRaw)
}

View File

@ -7,10 +7,8 @@ package outbound
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/v2rayA/dae/common/consts"
"github.com/v2rayA/dae/config"
"github.com/v2rayA/dae/pkg/config_parser"
"strconv"
)
@ -20,50 +18,38 @@ type DialerSelectionPolicy struct {
}
func NewDialerSelectionPolicyFromGroupParam(param *config.Group) (policy *DialerSelectionPolicy, err error) {
switch val := param.Policy.(type) {
case string:
switch consts.DialerSelectionPolicy(val) {
case consts.DialerSelectionPolicy_Random,
consts.DialerSelectionPolicy_MinAverage10Latencies,
consts.DialerSelectionPolicy_MinLastLatency,
consts.DialerSelectionPolicy_MinMovingAverageLatencies:
return &DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy(val),
}, nil
case consts.DialerSelectionPolicy_Fixed:
return nil, fmt.Errorf("%v need to specify node index", val)
default:
return nil, fmt.Errorf("unexpected policy: %v", val)
fs := config.FunctionListOrStringToFunction(param.Policy)
if len(fs) > 1 || len(fs) == 0 {
return nil, fmt.Errorf("policy should be exact 1 function: got %v", len(fs))
}
f := fs[0]
switch fName := consts.DialerSelectionPolicy(f.Name); fName {
case consts.DialerSelectionPolicy_Random,
consts.DialerSelectionPolicy_MinAverage10Latencies,
consts.DialerSelectionPolicy_MinLastLatency,
consts.DialerSelectionPolicy_MinMovingAverageLatencies:
return &DialerSelectionPolicy{
Policy: fName,
}, nil
case consts.DialerSelectionPolicy_Fixed:
if f.Not {
return nil, fmt.Errorf("policy param does not support not operator: !%v()", f.Name)
}
case []*config_parser.Function:
if len(val) > 1 || len(val) == 0 {
logrus.Debugf("%@", val)
return nil, fmt.Errorf("policy should be exact 1 function: got %v", len(val))
if len(f.Params) > 1 || f.Params[0].Key != "" {
return nil, fmt.Errorf(`invalid "%v" param format`, f.Name)
}
f := val[0]
switch consts.DialerSelectionPolicy(f.Name) {
case consts.DialerSelectionPolicy_Fixed:
// Should be like:
// policy: fixed(0)
if f.Not {
return nil, fmt.Errorf("policy param does not support not operator: !%v()", f.Name)
}
if len(f.Params) > 1 || f.Params[0].Key != "" {
return nil, fmt.Errorf(`invalid "%v" param format`, f.Name)
}
strIndex := f.Params[0].Val
index, err := strconv.Atoi(strIndex)
if len(f.Params) > 1 || f.Params[0].Key != "" {
return nil, fmt.Errorf(`invalid "%v" param format: %w`, f.Name, err)
}
return &DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy(f.Name),
FixedIndex: index,
}, nil
default:
return nil, fmt.Errorf("unexpected policy func: %v", f.Name)
strIndex := f.Params[0].Val
index, err := strconv.Atoi(strIndex)
if len(f.Params) > 1 || f.Params[0].Key != "" {
return nil, fmt.Errorf(`invalid "%v" param format: %w`, f.Name, err)
}
return &DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy(f.Name),
FixedIndex: index,
}, nil
default:
return nil, fmt.Errorf("unexpected param.Policy.(type): %T", val)
return nil, fmt.Errorf("unexpected policy: %v", f.Name)
}
}

View File

@ -42,7 +42,7 @@ func (b *RulesBuilder) RegisterFunctionParser(funcName string, parser FunctionPa
func (b *RulesBuilder) Apply(rules []*config_parser.RoutingRule) (err error) {
for _, rule := range rules {
b.log.Debugln("[rule]", rule.String(true))
b.log.Debugln("[rule]", rule.String(true, false, false))
outbound, err := ParseOutbound(&rule.Outbound)
if err != nil {
return err
@ -80,7 +80,7 @@ func (b *RulesBuilder) Apply(rules []*config_parser.RoutingRule) (err error) {
}
if err = functionParser(b.log, f, key, paramValueGroup, overrideOutbound); err != nil {
return fmt.Errorf("failed to parse '%v': %w", f.String(false), err)
return fmt.Errorf("failed to parse '%v': %w", f.String(false, false, false), err)
}
}
}

View File

@ -87,7 +87,7 @@ func (o *MergeAndSortRulesOptimizer) Optimize(rules []*config_parser.RoutingRule
if len(mergingRule.AndFunctions) == 1 &&
len(rules[i].AndFunctions) == 1 &&
mergingRule.AndFunctions[0].Name == rules[i].AndFunctions[0].Name &&
rules[i].Outbound.String(true) == mergingRule.Outbound.String(true) {
rules[i].Outbound.String(true, false, true) == mergingRule.Outbound.String(true, false, true) {
mergingRule.AndFunctions[0].Params = append(mergingRule.AndFunctions[0].Params, rules[i].AndFunctions[0].Params...)
} else {
newRules = append(newRules, mergingRule)
@ -134,10 +134,10 @@ func deduplicateParams(list []*config_parser.Param) []*config_parser.Param {
res := make([]*config_parser.Param, 0, len(list))
m := make(map[string]struct{})
for _, v := range list {
if _, ok := m[v.String(true)]; ok {
if _, ok := m[v.String(true, false)]; ok {
continue
}
m[v.String(true)] = struct{}{}
m[v.String(true, false)] = struct{}{}
res = append(res, v)
}
return res
@ -254,6 +254,8 @@ func (o *DatReaderOptimizer) Optimize(rules []*config_parser.RoutingRule) ([]*co
params, err = o.loadGeoSite(fields[0], fields[1])
case consts.Function_Ip:
params, err = o.loadGeoIp(fields[0], fields[1])
default:
return nil, fmt.Errorf("unsupported extension file extraction in function %v", f.Name)
}
default:
// Keep this param.

View File

@ -42,11 +42,24 @@ func FunctionOrStringToFunction(fs FunctionOrString) (f *config_parser.Function)
}
}
type FunctionListOrString interface{}
func FunctionListOrStringToFunction(fs FunctionListOrString) (f []*config_parser.Function) {
switch fs := fs.(type) {
case string:
return []*config_parser.Function{{Name: fs}}
case []*config_parser.Function:
return fs
default:
panic(fmt.Sprintf("unknown type of 'fallback' in section routing: %T", fs))
}
}
type Group struct {
Name string `mapstructure:"_"`
Filter []*config_parser.Function `mapstructure:"filter"`
Policy interface{} `mapstructure:"policy" required:""`
Policy FunctionListOrString `mapstructure:"policy" required:""`
}
type DnsRequestRouting struct {
@ -57,31 +70,32 @@ type DnsResponseRouting struct {
Rules []*config_parser.RoutingRule `mapstructure:"_"`
Fallback FunctionOrString `mapstructure:"fallback" required:""`
}
type DnsRouting struct {
Request DnsRequestRouting `mapstructure:"request"`
Response DnsResponseRouting `mapstructure:"response"`
}
type KeyableString string
type Dns struct {
Upstream []string `mapstructure:"upstream"`
Routing struct {
Request DnsRequestRouting `mapstructure:"request"`
Response DnsResponseRouting `mapstructure:"response"`
} `mapstructure:"routing"`
Upstream []KeyableString `mapstructure:"upstream"`
Routing DnsRouting `mapstructure:"routing"`
}
type Routing struct {
Rules []*config_parser.RoutingRule `mapstructure:"_"`
Fallback FunctionOrString `mapstructure:"fallback"`
Final FunctionOrString `mapstructure:"final"`
}
type Params struct {
Global Global `mapstructure:"global" required:""`
Subscription []string `mapstructure:"subscription"`
Node []string `mapstructure:"node"`
Group []Group `mapstructure:"group" required:""`
Routing Routing `mapstructure:"routing" required:""`
Dns Dns `mapstructure:"dns"`
type Config struct {
Global Global `mapstructure:"global" required:"" desc:"GlobalDesc"`
Subscription []KeyableString `mapstructure:"subscription"`
Node []KeyableString `mapstructure:"node"`
Group []Group `mapstructure:"group" required:"" desc:"GroupDesc"`
Routing Routing `mapstructure:"routing" required:""`
Dns Dns `mapstructure:"dns" desc:"DnsDesc"`
}
// New params from sections. This func assumes merging (section "include") and deduplication for section names has been executed.
func New(sections []*config_parser.Section) (params *Params, err error) {
func New(sections []*config_parser.Section) (conf *Config, err error) {
// Set up name to section for further use.
type Section struct {
Val *config_parser.Section
@ -92,9 +106,9 @@ func New(sections []*config_parser.Section) (params *Params, err error) {
nameToSection[section.Name] = &Section{Val: section}
}
params = &Params{}
conf = &Config{}
// Use specified parser to parse corresponding section.
_val := reflect.ValueOf(params)
_val := reflect.ValueOf(conf)
val := _val.Elem()
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
@ -134,9 +148,9 @@ func New(sections []*config_parser.Section) (params *Params, err error) {
// Apply config patches.
for _, patch := range patches {
if err = patch(params); err != nil {
if err = patch(conf); err != nil {
return nil, err
}
}
return params, nil
return conf, nil
}

View File

@ -132,10 +132,10 @@ func (m *Merger) dfsMerge(entry string, fatherEntry string) (err error) {
for _, include := range includes {
switch v := include.Value.(type) {
case *config_parser.Param:
nextEntry := v.String(true)
nextEntry := v.String(true, false)
patterEntries = append(patterEntries, filepath.Join(m.entryDir, nextEntry))
default:
return fmt.Errorf("unsupported include grammar in %v: %v", entry, include.String())
return fmt.Errorf("unsupported include grammar in %v: %v", entry, include.String(false, false))
}
}
// DFS and merge children recursively.

77
config/desc.go Normal file
View File

@ -0,0 +1,77 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
type Desc map[string]string
var SectionSummaryDesc = Desc{
"subscription": "Subscriptions defined here will be resolved as nodes and merged as a part of the global node pool.\nSupport to give the subscription a tag, and filter nodes from a given subscription in the group section.",
"node": "Nodes defined here will be merged as a part of the global node pool.",
"dns": "See more at https://github.com/v2rayA/dae/blob/main/docs/dns.md.",
"group": "Node group. Groups defined here can be used as outbounds in section \"routing\".",
"routing": `Traffic follows this routing. See https://github.com/v2rayA/dae/blob/main/docs/routing.md for full examples.
Notice: domain traffic split will fail if DNS traffic is not taken over by dae.
Built-in outbound: direct, must_direct, block.
Available functions: domain, sip, dip, sport, dport, ipversion, l4proto, pname, mac.
Available keys in domain function: suffix, keyword, regex, full. No key indicates suffix.
domain: Match domain.
sip: Match source IP. CIDR format is also supported.
dip: Match dest IP. CIDR format is also supported.
sport: Match source port. Range like 8000-9000 is also supported.
dport: Match dest port. Range like 8000-9000 is also supported.
ipversion: Match IP version. Available values: 4, 6.
l4proto: Match level 4 protocol. Available values: tcp, udp.
pname: Match process name. It only works on WAN mode and for localhost programs.
mac: Match source MAC address. It works on LAN mode.`,
}
var SectionDescription = map[string]Desc{
"GlobalDesc": GlobalDesc,
"DnsDesc": DnsDesc,
"GroupDesc": GroupDesc,
}
var GlobalDesc = Desc{
"tproxy_port": "tproxy port to listen at. It is NOT a HTTP/SOCKS port, and is just used by eBPF program.\nIn normal case, you do not need to use it.",
"log_level": "Log level: error, warn, info, debug, trace.",
"tcp_check_url": "Node connectivity check.\nHost of URL should have both IPv4 and IPv6 if you have double stack in local.\nConsidering traffic consumption, it is recommended to choose a site with anycast IP and less response.",
"udp_check_dns": "This DNS will be used to check UDP connectivity of nodes. And if dns_upstream below contains tcp, it also be used to check TCP DNS connectivity of nodes.\nThis DNS should have both IPv4 and IPv6 if you have double stack in local.",
"check_interval": "Interval of connectivity check for TCP and UDP",
"check_tolerance": "Group will switch node only when new_latency <= old_latency - tolerance.",
"lan_interface": "The LAN interface to bind. Use it if you only want to proxy LAN instead of localhost.",
"lan_nat_direct": "SNAT for incoming connection to avoid MAC learning.\nAlways set it true if you are NOT using dae as a transparent bridge, but will reduce forwarding performance for direct traffic in LAN mode.\nThis option does not affect direct traffic performance of WAN.",
"wan_interface": "The WAN interface to bind. Use it if you want to proxy localhost.",
"allow_insecure": "Allow insecure TLS certificates. It is not recommended to turn it on unless you have to.",
"dial_mode": `Optional values of dial_mode are:
1. "ip". Dial proxy using the IP from DNS directly. This allows your ipv4, ipv6 to choose the optimal path respectively, and makes the IP version requested by the application meet expectations. For example, if you use curl -4 ip.sb, you will request IPv4 via proxy and get a IPv4 echo. And curl -6 ip.sb will request IPv6. This may solve some wierd full-cone problem if your are be your node support that.
2. "domain". Dial proxy using the domain from sniffing. This will relieve DNS pollution problem to a great extent if have impure DNS environment. Generally, this mode brings faster proxy response time because proxy will re-resolve the domain in remote, thus get better IP result to connect. This policy does not impact routing. That is to say, domain rewrite will be after traffic split of routing and dae will not re-route it.
3. "domain+". Based on domain mode but do not check the authenticity of sniffing result. It is useful for users whose DNS requests do not go through dae but want faster proxy response time. Notice that, if DNS requests do not go through dae, dae cannot split traffic by domain.`,
}
var DnsDesc = Desc{
"upstream": "Value can be scheme://host:port, where the scheme can be tcp/udp/tcp+udp.\nIf host is a domain and has both IPv4 and IPv6 record, dae will automatically choose IPv4 or IPv6 to use according to group policy (such as min latency policy).\nPlease make sure DNS traffic will go through and be forwarded by dae, which is REQUIRED for domain routing.\nIf dial_mode is \"ip\", the upstream DNS answer SHOULD NOT be polluted, so domestic public DNS is not recommended.",
"request": `DNS requests will follow this routing.
Built-in outbound: asis.
Available functions: qname, qtype`,
"response": `DNS responses will follow this routing.
Built-in outbound: accept, reject.
Available functions: qname, qtype, ip, upstream`,
}
var GroupDesc = Desc{
"filter": `Filter nodes from the global node pool defined by the "subscription" and "node" sections.
Available functions: name, subtag. Not operator is supported.
Available keys in name function: keyword, regex. No key indicates full match.
Available keys in subtag function: regex. No key indicates full match.`,
"policy": `Dialer selection policy. For each new connection, select a node as dialer from group by this policy.
Available values: random, fixed, min, min_avg10, min_moving_avg.
random: Select randomly.
fixed: Select the fixed node. Connectivity check will be disabled.
min: Select node by the latency of last check.
min_avg10: Select node by the average of latencies of last 10 checks.
min_moving_avg: Select node by the moving average of latencies of checks, which means more recent latencies have higher weight.
`,
}

236
config/marshal.go Normal file
View File

@ -0,0 +1,236 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
import (
"bytes"
"fmt"
"github.com/v2rayA/dae/common"
"github.com/v2rayA/dae/pkg/config_parser"
"reflect"
"strconv"
"strings"
)
// Marshal assume all tokens should be legal, and does not prevent injection attacks.
func (c *Config) Marshal(indentSpace int) (b []byte, err error) {
m := marshaller{
indentSpace: indentSpace,
buf: new(bytes.Buffer),
}
// Root.
v := reflect.ValueOf(*c)
t := v.Type()
for i := 0; i < v.NumField(); i++ {
k, ok := t.Field(i).Tag.Lookup("mapstructure")
if !ok {
return nil, fmt.Errorf("section %v misses tag mapstructure", t.Field(i).Name)
}
if err = m.marshalSection(k, v.Field(i), 0); err != nil {
return nil, err
}
}
return m.buf.Bytes(), nil
}
type marshaller struct {
indentSpace int
buf *bytes.Buffer
}
func (m *marshaller) writeLine(depth int, line string) {
m.buf.Write(bytes.Repeat([]byte{' '}, depth*m.indentSpace))
m.buf.WriteString(line)
m.buf.WriteString("\n")
}
func (m *marshaller) marshalStringList(from reflect.Value, depth int, keyable bool) (err error) {
for i := 0; i < from.Len(); i++ {
str := from.Index(i)
if keyable {
tag, afterTag := common.GetTagFromLinkLikePlaintext(str.String())
if len(tag) > 0 {
m.writeLine(depth, tag+":"+strconv.Quote(afterTag))
continue
}
}
m.writeLine(depth, strconv.Quote(str.String()))
}
return nil
}
func (m *marshaller) marshalSection(name string, from reflect.Value, depth int) (err error) {
m.writeLine(depth, name+" {")
defer m.writeLine(depth, "}")
switch from.Kind() {
case reflect.Slice:
elemType := from.Type().Elem()
switch elemType.Kind() {
case reflect.String:
keyable := false
switch elemType {
case reflect.TypeOf(KeyableString("")):
keyable = true
default:
}
if err = m.marshalStringList(from, depth+1, keyable); err != nil {
return err
}
return nil
case reflect.Struct:
// "from" is a section list (sections in section).
/**
from {
field1 {
...
}
field2 {
...
}
}
should be parsed from:
from []struct {
Name string `mapstructure: "_"`
...
}
*/
// The struct should contain Name.
nameStructField, ok := elemType.FieldByName("Name")
if !ok || nameStructField.Type.Kind() != reflect.String || nameStructField.Tag.Get("mapstructure") != "_" {
return fmt.Errorf("a string field \"Name\" with mapstructure:\"_\" is required in struct %v from parse section", from.Type().Elem().String())
}
// Scan sections.
for i := 0; i < from.Len(); i++ {
item := from.Index(i)
nameField := item.FieldByName("Name")
if nameField.Kind() != reflect.String {
return fmt.Errorf("name field of section should be string type")
}
if err = m.marshalSection(nameField.String(), item, depth+1); err != nil {
return err
}
}
return nil
default:
goto unsupported
}
case reflect.Struct:
// Section.
return m.marshalParam(from, depth+1)
default:
goto unsupported
}
panic("code should not reach here")
unsupported:
return fmt.Errorf("unsupported section type %v", from.Type())
}
func (m *marshaller) marshalLeaf(key string, from reflect.Value, depth int) (err error) {
if from.IsZero() {
// Do not marshal zero value.
return nil
}
switch from.Kind() {
case reflect.Slice:
if from.Len() == 0 {
return nil
}
switch from.Index(0).Interface().(type) {
case fmt.Stringer, string,
uint8, uint16, uint32, uint64,
int8, int16, int32, int64,
float32, float64,
bool:
var vals []string
for i := 0; i < from.Len(); i++ {
vals = append(vals, fmt.Sprintf("%v", from.Index(i).Interface()))
}
m.writeLine(depth, key+":"+strconv.Quote(strings.Join(vals, ",")))
case *config_parser.Function:
var vals []string
for i := 0; i < from.Len(); i++ {
v := from.Index(i).Interface().(*config_parser.Function)
vals = append(vals, v.String(true, true, false))
}
m.writeLine(depth, key+":"+strings.Join(vals, "&&"))
case KeyableString:
m.writeLine(depth, key+" {")
if err = m.marshalStringList(from, depth+1, true); err != nil {
return err
}
m.writeLine(depth, "}")
default:
return fmt.Errorf("unknown leaf array type: %v", from.Type())
}
default:
switch val := from.Interface().(type) {
case fmt.Stringer, string,
uint8, uint16, uint32, uint64,
int8, int16, int32, int64,
float32, float64,
bool:
m.writeLine(depth, key+":"+strconv.Quote(fmt.Sprintf("%v", val)))
case *config_parser.Function:
m.writeLine(depth, key+":"+val.String(true, true, false))
default:
return fmt.Errorf("unknown leaf type: %T", val)
}
}
return nil
}
func (m *marshaller) marshalParam(from reflect.Value, depth int) (err error) {
if from.Kind() != reflect.Struct {
return fmt.Errorf("marshalParam can only marshal struct")
}
// Marshal section.
typ := from.Type()
for i := 0; i < from.NumField(); i++ {
field := from.Field(i)
structField := typ.Field(i)
key, ok := structField.Tag.Lookup("mapstructure")
if !ok {
return fmt.Errorf("tag mapstructure is required")
}
// Reserved field.
if key == "_" {
switch structField.Name {
case "Name":
case "Rules":
// Expand.
rules, ok := field.Interface().([]*config_parser.RoutingRule)
if !ok {
return fmt.Errorf("unexpected Rules type: %v", field.Type())
}
for _, r := range rules {
m.writeLine(depth, r.String(false, true, true))
}
default:
return fmt.Errorf("unknown reserved field: %v", structField.Name)
}
continue
}
// Section(s) field.
if field.Kind() == reflect.Struct || (field.Kind() == reflect.Slice &&
field.Type().Elem().Kind() == reflect.Struct) {
if err = m.marshalSection(key, field, depth); err != nil {
return err
}
continue
}
// Normal field.
if err = m.marshalLeaf(key, field, depth); err != nil {
return err
}
}
return nil
}

50
config/marshal_test.go Normal file
View File

@ -0,0 +1,50 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func TestMarshal(t *testing.T) {
abs, err := filepath.Abs("../example.dae")
if err != nil {
t.Fatal(err)
}
merger := NewMerger(abs)
sections, _, err := merger.Merge()
if err != nil {
t.Fatal(err)
}
conf1, err := New(sections)
if err != nil {
t.Fatal(err)
}
b, err := conf1.Marshal(2)
if err != nil {
t.Fatal(err)
}
t.Log(string(b))
// Read it again.
if err = os.WriteFile("/tmp/test.dae", b, 0640); err != nil {
t.Fatal(err)
}
sections, _, err = NewMerger("/tmp/test.dae").Merge()
if err != nil {
t.Fatal(err)
}
conf2, err := New(sections)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(conf1, conf2) {
t.Fatal("not equal")
}
}

114
config/outline.go Normal file
View File

@ -0,0 +1,114 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
import (
jsoniter "github.com/json-iterator/go"
"reflect"
"sort"
)
type Outline struct {
Version string `json:"version"`
Leaves []string `json:"leaves"`
Structure []*OutlineElem `json:"structure"`
}
type OutlineElem struct {
Name string `json:"name,omitempty"`
Mapping string `json:"mapping,omitempty"`
IsArray bool `json:"isArray,omitempty"`
Type string `json:"type,omitempty"`
ElemType string `json:"elemType,omitempty"`
Desc string `json:"desc,omitempty"`
Structure []*OutlineElem `json:"structure,omitempty"`
}
func ExportOutline(version string) *Outline {
// Get structure.
t := reflect.TypeOf(Config{})
exporter := outlineExporter{
leaves: make(map[string]reflect.Type),
pkgPathScope: t.PkgPath(),
}
structure := exporter.exportStruct(t, SectionSummaryDesc, false)
// Get string type leaves.
var leaves []string
for k := range exporter.leaves {
leaves = append(leaves, k)
}
sort.Strings(leaves)
return &Outline{
Version: version,
Leaves: leaves,
Structure: structure,
}
}
func ExportOutlineJson(version string) string {
b, err := jsoniter.MarshalIndent(ExportOutline(version), "", " ")
if err != nil {
panic(err)
}
return string(b)
}
type outlineExporter struct {
leaves map[string]reflect.Type
pkgPathScope string
}
func (e *outlineExporter) exportStruct(t reflect.Type, descSource Desc, inheritSource bool) (outlines []*OutlineElem) {
for i := 0; i < t.NumField(); i++ {
section := t.Field(i)
// Parse desc.
var desc string
if descSource != nil {
desc = descSource[section.Tag.Get("mapstructure")]
}
// Parse elem type.
var isArray bool
var typ reflect.Type
switch section.Type.Kind() {
case reflect.Slice:
typ = section.Type.Elem()
isArray = true
default:
typ = section.Type
}
if typ.Kind() == reflect.Pointer {
typ = typ.Elem()
}
// Parse children.
var children []*OutlineElem
switch typ.Kind() {
case reflect.Struct:
var nextDescSource Desc
if inheritSource {
nextDescSource = descSource
} else {
nextDescSource = SectionDescription[section.Tag.Get("desc")]
}
if typ.PkgPath() == "" || typ.PkgPath() == e.pkgPathScope {
children = e.exportStruct(typ, nextDescSource, true)
}
}
if len(children) == 0 {
// Record leaves.
e.leaves[typ.String()] = typ
}
outlines = append(outlines, &OutlineElem{
Name: section.Name,
Mapping: section.Tag.Get("mapstructure"),
IsArray: isArray,
Type: typ.String(),
Desc: desc,
Structure: children,
})
}
return outlines
}

14
config/outline_test.go Normal file
View File

@ -0,0 +1,14 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
import (
"testing"
)
func TestExportOutline(t *testing.T) {
t.Log(ExportOutlineJson("test"))
}

View File

@ -18,19 +18,18 @@ func StringListParser(to reflect.Value, section *config_parser.Section) error {
return fmt.Errorf("StringListParser can only unmarshal section to *[]string")
}
to = to.Elem()
if to.Type() != reflect.TypeOf([]string{}) {
if to.Type() != reflect.TypeOf([]string{}) &&
!(to.Kind() == reflect.Slice && to.Type().Elem().Kind() == reflect.String) {
return fmt.Errorf("StringListParser can only unmarshal section to *[]string")
}
var list []string
for _, item := range section.Items {
switch itemVal := item.Value.(type) {
case *config_parser.Param:
list = append(list, itemVal.String(true))
to.Set(reflect.Append(to, reflect.ValueOf(itemVal.String(true, false)).Convert(to.Type().Elem())))
default:
return fmt.Errorf("section %v does not support type %v: %v", section.Name, item.Type.String(), item.String())
return fmt.Errorf("section %v does not support type %v: %v", section.Name, item.Type.String(), item.String(false, false))
}
}
to.Set(reflect.ValueOf(list))
return nil
}
@ -85,7 +84,7 @@ func ParamParser(to reflect.Value, section *config_parser.Section, ignoreType []
switch itemVal := item.Value.(type) {
case *config_parser.Param:
if itemVal.Key == "" {
return fmt.Errorf("unsupported text without a key: %v", itemVal.String(true))
return fmt.Errorf("unsupported text without a key: %v", itemVal.String(true, false))
}
field, ok := keyToField[itemVal.Key]
if !ok {
@ -138,7 +137,7 @@ func ParamParser(to reflect.Value, section *config_parser.Section, ignoreType []
// Assign. "to" should have field "Rules".
structField, ok := to.Type().FieldByName("Rules")
if !ok || structField.Type != reflect.TypeOf([]*config_parser.RoutingRule{}) {
return fmt.Errorf("unexpected type: \"routing rule\": %v", itemVal.String(true))
return fmt.Errorf("unexpected type: \"routing rule\": %v", itemVal.String(true, false, false))
}
if structField.Tag.Get("mapstructure") != "_" {
return fmt.Errorf("a []*RoutingRule field \"Rules\" with mapstructure:\"_\" is required in struct %v to parse section", to.Type().String())
@ -147,7 +146,7 @@ func ParamParser(to reflect.Value, section *config_parser.Section, ignoreType []
field.Set(reflect.Append(field, reflect.ValueOf(itemVal)))
default:
if _, ignore := ignoreTypeSet[reflect.TypeOf(itemVal)]; !ignore {
return fmt.Errorf("unexpected type %v: %v", item.Type.String(), item.String())
return fmt.Errorf("unexpected type %v: %v", item.Type.String(), item.String(false, false))
}
}
}

View File

@ -7,32 +7,17 @@ package config
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/v2rayA/dae/common/consts"
)
type patch func(params *Params) error
type patch func(params *Config) error
var patches = []patch{
patchRoutingFallback,
patchEmptyDns,
patchDeprecatedGlobalDnsUpstream,
}
func patchRoutingFallback(params *Params) error {
// We renamed final as fallback. So we apply this patch for compatibility with older users.
if params.Routing.Fallback == nil && params.Routing.Final != nil {
params.Routing.Fallback = params.Routing.Final
logrus.Warnln("Name 'final' in section routing was deprecated and will be removed in the future; please rename it as 'fallback'")
}
// Fallback is required.
if params.Routing.Fallback == nil {
return fmt.Errorf("fallback is required in routing")
}
return nil
}
func patchEmptyDns(params *Params) error {
func patchEmptyDns(params *Config) error {
if params.Dns.Routing.Request.Fallback == nil {
params.Dns.Routing.Request.Fallback = consts.DnsRequestOutboundIndex_AsIs.String()
}
@ -42,9 +27,10 @@ func patchEmptyDns(params *Params) error {
return nil
}
func patchDeprecatedGlobalDnsUpstream(params *Params) error {
func patchDeprecatedGlobalDnsUpstream(params *Config) error {
if params.Global.DnsUpstream != "<empty>" {
return fmt.Errorf("'global.dns_upstream' was deprecated, please refer to the latest examples and docs for help")
}
params.Global.DnsUpstream = ""
return nil
}

View File

@ -283,7 +283,7 @@ func NewControlPlane(
if log.IsLevelEnabled(logrus.DebugLevel) {
var debugBuilder strings.Builder
for _, rule := range rules {
debugBuilder.WriteString(rule.String(true) + "\n")
debugBuilder.WriteString(rule.String(true, false, false) + "\n")
}
log.Debugf("RoutingA:\n%vfallback: %v\n", debugBuilder.String(), routingA.Fallback)
}

View File

@ -26,8 +26,8 @@ global {
#lan_interface: docker0
# SNAT for incoming connection to avoid MAC learning.
# Set it true if you are NOT using dae as a transparent bridge, but will reduce forwarding
# performance for direct traffic.
# Always set it true if you are NOT using dae as a transparent bridge, but will reduce forwarding
# performance for direct traffic in LAN mode.
# This option does not affect direct traffic performance of WAN.
lan_nat_direct: true
@ -82,13 +82,15 @@ dns {
alidns: 'udp://dns.alidns.com:53'
googledns: 'tcp+udp://dns.google:53'
}
request {
fallback: asis
}
response {
upstream(googledns) -> accept
!qname(geosite:cn) && ip(geoip:private) -> googledns
fallback: accept
routing {
request {
fallback: asis
}
response {
upstream(googledns) -> accept
!qname(geosite:cn) && ip(geoip:private) -> googledns
fallback: accept
}
}
}

View File

@ -58,17 +58,17 @@ type Item struct {
Value interface{}
}
func (i *Item) String() string {
func (i *Item) String(compact bool, quoteVal bool) string {
var builder strings.Builder
builder.WriteString("type: " + i.Type.String() + "\n")
var content string
switch val := i.Value.(type) {
case *RoutingRule:
content = val.String(false)
content = val.String(false, compact, quoteVal)
case *Param:
content = val.String(false)
content = val.String(false, quoteVal)
case *Section:
content = val.String()
content = val.String(compact, quoteVal)
default:
return "<Unknown>\n"
}
@ -85,12 +85,12 @@ type Section struct {
Items []*Item
}
func (s *Section) String() string {
func (s *Section) String(compact bool, quoteVal bool) string {
var builder strings.Builder
builder.WriteString("section: " + s.Name + "\n")
var strItemList []string
for _, item := range s.Items {
lines := strings.Split(item.String(), "\n")
lines := strings.Split(item.String(compact, quoteVal), "\n")
for i := range lines {
lines[i] = "\t" + lines[i]
}
@ -109,21 +109,27 @@ type Param struct {
AndFunctions []*Function
}
func (p *Param) String(compact bool) string {
func (p *Param) String(compact bool, quoteVal bool) string {
var quote func(string) string
if quoteVal {
quote = strconv.Quote
} else {
quote = func(s string) string { return s }
}
if p.Key == "" {
return p.Val
return quote(p.Val)
}
if p.AndFunctions != nil {
a := paramAndFunctions{
Key: p.Key,
AndFunctions: p.AndFunctions,
}
return a.String(compact)
return a.String(compact, quoteVal)
}
if compact {
return p.Key + ":" + p.Val
return p.Key + ":" + quote(p.Val)
} else {
return p.Key + ": " + p.Val
return p.Key + ": " + quote(p.Val)
}
}
@ -133,22 +139,25 @@ type Function struct {
Params []*Param
}
func (f *Function) String(compact bool) string {
func (f *Function) String(compact bool, quoteVal bool, omitEmpty bool) string {
var builder strings.Builder
if f.Not {
builder.WriteString("!")
}
builder.WriteString(f.Name + "(")
var strParamList []string
for _, p := range f.Params {
strParamList = append(strParamList, p.String(compact))
builder.WriteString(f.Name)
if !(omitEmpty && len(f.Params) == 0) {
builder.WriteString("(")
var strParamList []string
for _, p := range f.Params {
strParamList = append(strParamList, p.String(compact, quoteVal))
}
if compact {
builder.WriteString(strings.Join(strParamList, ","))
} else {
builder.WriteString(strings.Join(strParamList, ", "))
}
builder.WriteString(")")
}
if compact {
builder.WriteString(strings.Join(strParamList, ","))
} else {
builder.WriteString(strings.Join(strParamList, ", "))
}
builder.WriteString(")")
return builder.String()
}
@ -157,7 +166,7 @@ type paramAndFunctions struct {
AndFunctions []*Function
}
func (p *paramAndFunctions) String(compact bool) string {
func (p *paramAndFunctions) String(compact bool, quoteVal bool) string {
var builder strings.Builder
if compact {
builder.WriteString(p.Key + ":")
@ -166,7 +175,7 @@ func (p *paramAndFunctions) String(compact bool) string {
}
var strFunctionList []string
for _, f := range p.AndFunctions {
strFunctionList = append(strFunctionList, f.String(compact))
strFunctionList = append(strFunctionList, f.String(compact, quoteVal, false))
}
if compact {
builder.WriteString(strings.Join(strFunctionList, "&&"))
@ -181,12 +190,16 @@ type RoutingRule struct {
Outbound Function
}
func (r *RoutingRule) String(replaceParamWithN bool) string {
func (r *RoutingRule) String(replaceParamWithN bool, compact bool, quoteVal bool) string {
var builder strings.Builder
var n int
for i, f := range r.AndFunctions {
if i != 0 {
builder.WriteString(" && ")
if compact {
builder.WriteString("&&")
} else {
builder.WriteString(" && ")
}
}
var paramBuilder strings.Builder
n += len(f.Params)
@ -195,9 +208,13 @@ func (r *RoutingRule) String(replaceParamWithN bool) string {
} else {
for j, param := range f.Params {
if j != 0 {
paramBuilder.WriteString(", ")
if compact {
paramBuilder.WriteString(",")
} else {
paramBuilder.WriteString(", ")
}
}
paramBuilder.WriteString(param.String(false))
paramBuilder.WriteString(param.String(compact, quoteVal))
}
}
symNot := ""
@ -206,6 +223,10 @@ func (r *RoutingRule) String(replaceParamWithN bool) string {
}
builder.WriteString(fmt.Sprintf("%v%v(%v)", symNot, f.Name, paramBuilder.String()))
}
builder.WriteString(" -> " + r.Outbound.String(true))
if compact {
builder.WriteString("->" + r.Outbound.String(compact, quoteVal, true))
} else {
builder.WriteString(" -> " + r.Outbound.String(compact, quoteVal, true))
}
return builder.String()
}

View File

@ -62,8 +62,8 @@ func (p *paramParser) parseParam(ctx *dae_config.ParameterContext) *Param {
func (p *paramParser) parseNonEmptyParamList(ctx *dae_config.NonEmptyParameterListContext) {
children := ctx.GetChildren()
if len(children) == 3 {
p.list = append(p.list, p.parseParam(children[2].(*dae_config.ParameterContext)))
p.parseNonEmptyParamList(children[0].(*dae_config.NonEmptyParameterListContext))
p.list = append(p.list, p.parseParam(children[2].(*dae_config.ParameterContext)))
} else if len(children) == 1 {
p.list = append(p.list, p.parseParam(children[0].(*dae_config.ParameterContext)))
}