Browse Source

merge ext into core

Darien Raymond 6 years ago
parent
commit
4eb2b5e607
71 changed files with 6346 additions and 18 deletions
  1. 10 10
      app/router/condition_geoip_test.go
  2. 10 6
      app/router/condition_test.go
  3. 44 0
      common/platform/filesystem/file.go
  4. 5 0
      infra/bazel/BUILD
  5. 66 0
      infra/bazel/build.bzl
  6. 23 0
      infra/bazel/gpg.bzl
  7. 21 0
      infra/bazel/matrix.bzl
  8. 164 0
      infra/bazel/zip.bzl
  9. 39 0
      infra/conf/api.go
  10. 53 0
      infra/conf/blackhole.go
  11. 34 0
      infra/conf/blackhole_test.go
  12. 7 0
      infra/conf/buildable.go
  13. 48 0
      infra/conf/command/command.go
  14. 9 0
      infra/conf/command/errors.generated.go
  15. 189 0
      infra/conf/common.go
  16. 211 0
      infra/conf/common_test.go
  17. 3 0
      infra/conf/conf.go
  18. 176 0
      infra/conf/dns.go
  19. 12 0
      infra/conf/dns_proxy.go
  20. 103 0
      infra/conf/dns_test.go
  21. 28 0
      infra/conf/dokodemo.go
  22. 41 0
      infra/conf/dokodemo_test.go
  23. 9 0
      infra/conf/errors.generated.go
  24. 57 0
      infra/conf/freedom.go
  25. 43 0
      infra/conf/freedom_test.go
  26. 36 0
      infra/conf/general_test.go
  27. 35 0
      infra/conf/http.go
  28. 39 0
      infra/conf/http_test.go
  29. 133 0
      infra/conf/json/reader.go
  30. 97 0
      infra/conf/json/reader_test.go
  31. 83 0
      infra/conf/loader.go
  32. 57 0
      infra/conf/log.go
  33. 69 0
      infra/conf/mtproto.go
  34. 40 0
      infra/conf/mtproto_test.go
  35. 96 0
      infra/conf/policy.go
  36. 40 0
      infra/conf/policy_test.go
  37. 56 0
      infra/conf/reverse.go
  38. 45 0
      infra/conf/reverse_test.go
  39. 502 0
      infra/conf/router.go
  40. 233 0
      infra/conf/router_test.go
  41. 9 0
      infra/conf/serial/errors.generated.go
  42. 71 0
      infra/conf/serial/loader.go
  43. 63 0
      infra/conf/serial/loader_test.go
  44. 3 0
      infra/conf/serial/serial.go
  45. 139 0
      infra/conf/shadowsocks.go
  46. 36 0
      infra/conf/shadowsocks_test.go
  47. 99 0
      infra/conf/socks.go
  48. 92 0
      infra/conf/socks_test.go
  49. 89 0
      infra/conf/transport.go
  50. 223 0
      infra/conf/transport_authenticators.go
  51. 476 0
      infra/conf/transport_internet.go
  52. 169 0
      infra/conf/transport_test.go
  53. 446 0
      infra/conf/v2ray.go
  54. 338 0
      infra/conf/v2ray_test.go
  55. 164 0
      infra/conf/vmess.go
  56. 117 0
      infra/conf/vmess_test.go
  57. 144 0
      infra/control/api.go
  58. 139 0
      infra/control/cert.go
  59. 51 0
      infra/control/command.go
  60. 3 0
      infra/control/control.go
  61. 9 0
      infra/control/errors.generated.go
  62. 70 0
      infra/control/fetch.go
  63. 53 0
      infra/control/love.go
  64. 8 0
      infra/control/main/BUILD
  65. 48 0
      infra/control/main/main.go
  66. 56 0
      infra/control/main/targets.bzl
  67. 31 0
      infra/control/uuid.go
  68. 137 0
      infra/control/verify.go
  69. 95 0
      infra/vprotogen/main.go
  70. 1 1
      main/jsonem/jsonem.go
  71. 1 1
      proto.go

+ 10 - 10
app/router/condition_geoip_test.go

@@ -10,9 +10,17 @@ import (
 	"v2ray.com/core/common"
 	"v2ray.com/core/common/net"
 	"v2ray.com/core/common/platform"
-	"v2ray.com/ext/sysio"
+	"v2ray.com/core/common/platform/filesystem"
 )
 
+func init() {
+	wd, err := os.Getwd()
+	common.Must(err)
+
+	common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat")))
+	common.Must(filesystem.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(wd, "..", "..", "release", "config", "geosite.dat")))
+}
+
 func TestGeoIPMatcherContainer(t *testing.T) {
 	container := &router.GeoIPMatcherContainer{}
 
@@ -112,8 +120,6 @@ func TestGeoIPMatcher(t *testing.T) {
 }
 
 func TestGeoIPMatcher4CN(t *testing.T) {
-	common.Must(sysio.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(os.Getenv("GOPATH"), "src", "v2ray.com", "core", "release", "config", "geoip.dat")))
-
 	ips, err := loadGeoIP("CN")
 	common.Must(err)
 
@@ -126,8 +132,6 @@ func TestGeoIPMatcher4CN(t *testing.T) {
 }
 
 func TestGeoIPMatcher6US(t *testing.T) {
-	common.Must(sysio.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(os.Getenv("GOPATH"), "src", "v2ray.com", "core", "release", "config", "geoip.dat")))
-
 	ips, err := loadGeoIP("US")
 	common.Must(err)
 
@@ -140,7 +144,7 @@ func TestGeoIPMatcher6US(t *testing.T) {
 }
 
 func loadGeoIP(country string) ([]*router.CIDR, error) {
-	geoipBytes, err := sysio.ReadAsset("geoip.dat")
+	geoipBytes, err := filesystem.ReadAsset("geoip.dat")
 	if err != nil {
 		return nil, err
 	}
@@ -159,8 +163,6 @@ func loadGeoIP(country string) ([]*router.CIDR, error) {
 }
 
 func BenchmarkGeoIPMatcher4CN(b *testing.B) {
-	common.Must(sysio.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(os.Getenv("GOPATH"), "src", "v2ray.com", "core", "release", "config", "geoip.dat")))
-
 	ips, err := loadGeoIP("CN")
 	common.Must(err)
 
@@ -175,8 +177,6 @@ func BenchmarkGeoIPMatcher4CN(b *testing.B) {
 }
 
 func BenchmarkGeoIPMatcher6US(b *testing.B) {
-	common.Must(sysio.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(os.Getenv("GOPATH"), "src", "v2ray.com", "core", "release", "config", "geoip.dat")))
-
 	ips, err := loadGeoIP("US")
 	common.Must(err)
 

+ 10 - 6
app/router/condition_test.go

@@ -15,12 +15,20 @@ import (
 	"v2ray.com/core/common/errors"
 	"v2ray.com/core/common/net"
 	"v2ray.com/core/common/platform"
+	"v2ray.com/core/common/platform/filesystem"
 	"v2ray.com/core/common/protocol"
 	"v2ray.com/core/common/protocol/http"
 	"v2ray.com/core/common/session"
-	"v2ray.com/ext/sysio"
 )
 
+func init() {
+	wd, err := os.Getwd()
+	common.Must(err)
+
+	common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat")))
+	common.Must(filesystem.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(wd, "..", "..", "release", "config", "geosite.dat")))
+}
+
 func withOutbound(outbound *session.Outbound) context.Context {
 	return session.ContextWithOutbound(context.Background(), outbound)
 }
@@ -246,7 +254,7 @@ func TestRoutingRule(t *testing.T) {
 }
 
 func loadGeoSite(country string) ([]*Domain, error) {
-	geositeBytes, err := sysio.ReadAsset("geosite.dat")
+	geositeBytes, err := filesystem.ReadAsset("geosite.dat")
 	if err != nil {
 		return nil, err
 	}
@@ -265,8 +273,6 @@ func loadGeoSite(country string) ([]*Domain, error) {
 }
 
 func TestChinaSites(t *testing.T) {
-	common.Must(sysio.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(os.Getenv("GOPATH"), "src", "v2ray.com", "core", "release", "config", "geosite.dat")))
-
 	domains, err := loadGeoSite("CN")
 	common.Must(err)
 
@@ -309,8 +315,6 @@ func TestChinaSites(t *testing.T) {
 }
 
 func BenchmarkMultiGeoIPMatcher(b *testing.B) {
-	common.Must(sysio.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(os.Getenv("GOPATH"), "src", "v2ray.com", "core", "release", "config", "geoip.dat")))
-
 	var geoips []*GeoIP
 
 	{

+ 44 - 0
common/platform/filesystem/file.go

@@ -0,0 +1,44 @@
+package filesystem
+
+import (
+	"io"
+	"os"
+
+	"v2ray.com/core/common/buf"
+	"v2ray.com/core/common/platform"
+)
+
+type FileReaderFunc func(path string) (io.ReadCloser, error)
+
+var NewFileReader FileReaderFunc = func(path string) (io.ReadCloser, error) {
+	return os.Open(path)
+}
+
+func ReadFile(path string) ([]byte, error) {
+	reader, err := NewFileReader(path)
+	if err != nil {
+		return nil, err
+	}
+	defer reader.Close()
+
+	return buf.ReadAllToBytes(reader)
+}
+
+func ReadAsset(file string) ([]byte, error) {
+	return ReadFile(platform.GetAssetLocation(file))
+}
+
+func CopyFile(dst string, src string) error {
+	bytes, err := ReadFile(src)
+	if err != nil {
+		return err
+	}
+	f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0644)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	_, err = f.Write(bytes)
+	return err
+}

+ 5 - 0
infra/bazel/BUILD

@@ -0,0 +1,5 @@
+filegroup(
+   name = "rules",
+   srcs = glob(["*.bzl"]),
+   visibility = ["//visibility:public"],
+)

+ 66 - 0
infra/bazel/build.bzl

@@ -0,0 +1,66 @@
+def _go_command(ctx):
+  output = ctx.attr.output
+  if ctx.attr.os == "windows":
+    output = output + ".exe"
+
+  output_file = ctx.actions.declare_file(ctx.attr.os + "/" + ctx.attr.arch + "/" + output)
+  pkg = ctx.attr.pkg
+
+  ld_flags = "-s -w"
+  if ctx.attr.ld:
+    ld_flags = ld_flags + " " + ctx.attr.ld
+
+  options = [
+    "go",
+    "build",
+    "-o", output_file.path,
+    "-compiler", "gc",
+    "-gcflags", '"all=-trimpath=${GOPATH}/src"',
+    "-asmflags", '"all=-trimpath=${GOPATH}/src"',
+    "-ldflags", "'%s'" % ld_flags,
+    "-tags", "'%s'" % ctx.attr.gotags,
+    pkg,
+  ]
+
+  command = " ".join(options)
+
+  envs = [
+    "CGO_ENABLED=0",
+    "GOOS="+ctx.attr.os,
+    "GOARCH="+ctx.attr.arch,
+    "GOROOT_FINAL=/go"
+  ]
+  
+  if ctx.attr.mips: # https://github.com/golang/go/issues/27260
+    envs+=["GOMIPS="+ctx.attr.mips]
+    envs+=["GOMIPS64="+ctx.attr.mips]
+    envs+=["GOMIPSLE="+ctx.attr.mips]
+    envs+=["GOMIPS64LE="+ctx.attr.mips]
+  if ctx.attr.arm:
+    envs+=["GOARM="+ctx.attr.arm]
+
+  command = " ".join(envs) + " " + command
+
+  ctx.actions.run_shell(
+    outputs = [output_file],
+    command = command,
+    use_default_shell_env = True,
+  )
+  runfiles = ctx.runfiles(files = [output_file])
+  return [DefaultInfo(executable = output_file, runfiles = runfiles)]
+
+
+foreign_go_binary = rule(
+  _go_command,
+  attrs = {
+    'pkg': attr.string(),
+    'output': attr.string(),
+    'os': attr.string(mandatory=True),
+    'arch': attr.string(mandatory=True),
+    'mips': attr.string(),
+    'arm': attr.string(),
+    'ld': attr.string(),
+    'gotags': attr.string(),
+  },
+  executable = True,
+)

+ 23 - 0
infra/bazel/gpg.bzl

@@ -0,0 +1,23 @@
+def _gpg_sign_impl(ctx):
+  output_file = ctx.actions.declare_file(ctx.file.base.basename + ctx.attr.suffix, sibling = ctx.file.base)
+  if not ctx.configuration.default_shell_env.get("GPG_PASS"):
+    ctx.actions.write(output_file, "")
+  else:
+    command = "echo ${GPG_PASS} | gpg --pinentry-mode loopback --digest-algo SHA512 --passphrase-fd 0 --output %s --detach-sig %s" % (output_file.path, ctx.file.base.path)
+    ctx.actions.run_shell(
+      command = command,
+      use_default_shell_env = True,
+      inputs = [ctx.file.base],
+      outputs = [output_file],
+      progress_message = "Signing binary",
+      mnemonic = "gpg",
+    )
+  return [DefaultInfo(files = depset([output_file]))]
+
+gpg_sign = rule(
+  implementation = _gpg_sign_impl,
+  attrs = {
+    "base": attr.label(allow_single_file=True),
+    "suffix": attr.string(default=".sig"),
+  },
+)

+ 21 - 0
infra/bazel/matrix.bzl

@@ -0,0 +1,21 @@
+SUPPORTED_MATRIX = [
+  ("windows", "amd64"),
+  ("windows", "386"),
+  ("darwin", "amd64"),
+  ("linux", "amd64"),
+  ("linux", "386"),
+  ("linux", "arm64"),
+  ("linux", "arm"),
+  ("linux", "mips64"),
+  ("linux", "mips"),
+  ("linux", "mips64le"),
+  ("linux", "mipsle"),
+  ("linux", "ppc64"),
+  ("linux", "ppc64le"),
+  ("linux", "s390x"),
+  ("freebsd", "amd64"),
+  ("freebsd", "386"),
+  ("openbsd", "amd64"),
+  ("openbsd", "386"),
+  ("dragonfly", "amd64"),
+]

+ 164 - 0
infra/bazel/zip.bzl

@@ -0,0 +1,164 @@
+# Copied from google/nomulus project as we don't want to import the whole repository.
+
+ZIPPER = "@bazel_tools//tools/zip:zipper"
+
+def long_path(ctx, file_):
+    """Constructs canonical runfile path relative to TEST_SRCDIR.
+    Args:
+      ctx: A Skylark rule context.
+      file_: A File object that should appear in the runfiles for the test.
+    Returns:
+      A string path relative to TEST_SRCDIR suitable for use in tests and
+      testing infrastructure.
+    """
+    if file_.short_path.startswith("../"):
+        return file_.short_path[3:]
+    if file_.owner and file_.owner.workspace_root:
+        return file_.owner.workspace_root + "/" + file_.short_path
+    return ctx.workspace_name + "/" + file_.short_path
+
+def collect_runfiles(targets):
+    """Aggregates runfiles from targets.
+    Args:
+      targets: A list of Bazel targets.
+    Returns:
+      A list of Bazel files.
+    """
+    data = depset()
+    for target in targets:
+        if hasattr(target, "runfiles"):
+            data += target.runfiles.files
+            continue
+        if hasattr(target, "data_runfiles"):
+            data += target.data_runfiles.files
+        if hasattr(target, "default_runfiles"):
+            data += target.default_runfiles.files
+    return data
+
+def _get_runfiles(target, attribute):
+    runfiles = getattr(target, attribute, None)
+    if runfiles:
+        return runfiles.files
+    return []
+
+def _zip_file(ctx):
+    """Implementation of zip_file() rule."""
+    for s, d in ctx.attr.mappings.items():
+        if (s.startswith("/") or s.endswith("/") or
+            d.startswith("/") or d.endswith("/")):
+            fail("mappings should not begin or end with slash")
+    srcs = depset()
+    srcs += ctx.files.srcs
+    srcs += ctx.files.data
+    srcs += collect_runfiles(ctx.attr.data)
+    mapped = _map_sources(ctx, srcs, ctx.attr.mappings)
+    cmd = [
+        "#!/bin/sh",
+        "set -e",
+        'repo="$(pwd)"',
+        'zipper="${repo}/%s"' % ctx.file._zipper.path,
+        'archive="${repo}/%s"' % ctx.outputs.out.path,
+        'tmp="$(mktemp -d "${TMPDIR:-/tmp}/zip_file.XXXXXXXXXX")"',
+        'cd "${tmp}"',
+    ]
+    cmd += [
+        '"${zipper}" x "${repo}/%s"' % dep.zip_file.path
+        for dep in ctx.attr.deps
+    ]
+    cmd += ["rm %s" % filename for filename in ctx.attr.exclude]
+    cmd += [
+        'mkdir -p "${tmp}/%s"' % zip_path
+        for zip_path in depset(
+            [
+                zip_path[:zip_path.rindex("/")]
+                for _, zip_path in mapped
+                if "/" in zip_path
+            ],
+        )
+    ]
+    cmd += [
+        'ln -sf "${repo}/%s" "${tmp}/%s"' % (path, zip_path)
+        for path, zip_path in mapped
+    ]
+    cmd += [
+        ("find . | sed 1d | cut -c 3- | LC_ALL=C sort" +
+         ' | xargs "${zipper}" cC "${archive}"'),
+        'cd "${repo}"',
+        'rm -rf "${tmp}"',
+    ]
+    script = ctx.new_file(ctx.bin_dir, "%s.sh" % ctx.label.name)
+    ctx.file_action(output = script, content = "\n".join(cmd), executable = True)
+    inputs = [ctx.file._zipper]
+    inputs += [dep.zip_file for dep in ctx.attr.deps]
+    inputs += list(srcs)
+    ctx.action(
+        inputs = inputs,
+        outputs = [ctx.outputs.out],
+        executable = script,
+        mnemonic = "zip",
+        progress_message = "Creating zip with %d inputs %s" % (
+            len(inputs),
+            ctx.label,
+        ),
+    )
+    return struct(files = depset([ctx.outputs.out]), zip_file = ctx.outputs.out)
+
+def _map_sources(ctx, srcs, mappings):
+    """Calculates paths in zip file for srcs."""
+
+    # order mappings with more path components first
+    mappings = sorted([
+        (-len(source.split("/")), source, dest)
+        for source, dest in mappings.items()
+    ])
+
+    # get rid of the integer part of tuple used for sorting
+    mappings = [(source, dest) for _, source, dest in mappings]
+    mappings_indexes = range(len(mappings))
+    used = {i: False for i in mappings_indexes}
+    mapped = []
+    for file_ in srcs:
+        run_path = long_path(ctx, file_)
+        zip_path = None
+        for i in mappings_indexes:
+            source = mappings[i][0]
+            dest = mappings[i][1]
+            if not source:
+                if dest:
+                    zip_path = dest + "/" + run_path
+                else:
+                    zip_path = run_path
+            elif source == run_path:
+                if dest:
+                    zip_path = dest
+                else:
+                    zip_path = run_path
+            elif run_path.startswith(source + "/"):
+                if dest:
+                    zip_path = dest + run_path[len(source):]
+                else:
+                    zip_path = run_path[len(source) + 1:]
+            else:
+                continue
+            used[i] = True
+            break
+        if not zip_path:
+            fail("no mapping matched: " + run_path)
+        mapped += [(file_.path, zip_path)]
+    for i in mappings_indexes:
+        if not used[i]:
+            fail('superfluous mapping: "%s" -> "%s"' % mappings[i])
+    return mapped
+
+pkg_zip = rule(
+    implementation = _zip_file,
+    attrs = {
+        "out": attr.output(mandatory = True),
+        "srcs": attr.label_list(allow_files = True),
+        "data": attr.label_list(allow_files = True),
+        "deps": attr.label_list(providers = ["zip_file"]),
+        "exclude": attr.string_list(),
+        "mappings": attr.string_dict(),
+        "_zipper": attr.label(default = Label(ZIPPER), single_file = True),
+    },
+)

+ 39 - 0
infra/conf/api.go

@@ -0,0 +1,39 @@
+package conf
+
+import (
+	"strings"
+
+	"v2ray.com/core/app/commander"
+	loggerservice "v2ray.com/core/app/log/command"
+	handlerservice "v2ray.com/core/app/proxyman/command"
+	statsservice "v2ray.com/core/app/stats/command"
+	"v2ray.com/core/common/serial"
+)
+
+type ApiConfig struct {
+	Tag      string   `json:"tag"`
+	Services []string `json:"services"`
+}
+
+func (c *ApiConfig) Build() (*commander.Config, error) {
+	if len(c.Tag) == 0 {
+		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 "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 - 0
infra/conf/blackhole.go

@@ -0,0 +1,53 @@
+package conf
+
+import (
+	"encoding/json"
+
+	"github.com/golang/protobuf/proto"
+
+	"v2ray.com/core/common/serial"
+	"v2ray.com/core/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 - 0
infra/conf/blackhole_test.go

@@ -0,0 +1,34 @@
+package conf_test
+
+import (
+	"testing"
+
+	"v2ray.com/core/common/serial"
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/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 - 0
infra/conf/buildable.go

@@ -0,0 +1,7 @@
+package conf
+
+import "github.com/golang/protobuf/proto"
+
+type Buildable interface {
+	Build() (proto.Message, error)
+}

+ 48 - 0
infra/conf/command/command.go

@@ -0,0 +1,48 @@
+package command
+
+//go:generate errorgen
+
+import (
+	"os"
+
+	"github.com/gogo/protobuf/proto"
+	"v2ray.com/core/common"
+	"v2ray.com/core/infra/conf/serial"
+	"v2ray.com/core/infra/control"
+)
+
+type ConfigCommand struct{}
+
+func (c *ConfigCommand) Name() string {
+	return "config"
+}
+
+func (c *ConfigCommand) Description() control.Description {
+	return control.Description{
+		Short: "Convert config among different formats.",
+		Usage: []string{
+			"v2ctl config",
+		},
+	}
+}
+
+func (c *ConfigCommand) Execute(args []string) error {
+	pbConfig, err := serial.LoadJSONConfig(os.Stdin)
+	if err != nil {
+		return newError("failed to parse json config").Base(err)
+	}
+
+	bytesConfig, err := proto.Marshal(pbConfig)
+	if err != nil {
+		return newError("failed to marshal proto config").Base(err)
+	}
+
+	if _, err := os.Stdout.Write(bytesConfig); err != nil {
+		return newError("failed to write proto config").Base(err)
+	}
+	return nil
+}
+
+func init() {
+	common.Must(control.RegisterCommand(&ConfigCommand{}))
+}

+ 9 - 0
infra/conf/command/errors.generated.go

@@ -0,0 +1,9 @@
+package command
+
+import "v2ray.com/core/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 189 - 0
infra/conf/common.go

@@ -0,0 +1,189 @@
+package conf
+
+import (
+	"encoding/json"
+	"os"
+	"strings"
+
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/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
+	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(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
+	}
+	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("Config: 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
+}
+
+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 := parseStringPort(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 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),
+	}
+}

+ 211 - 0
infra/conf/common_test.go

@@ -0,0 +1,211 @@
+package conf_test
+
+import (
+	"encoding/json"
+	"os"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"v2ray.com/core/common/protocol"
+
+	"v2ray.com/core/common"
+	"v2ray.com/core/common/net"
+	. "v2ray.com/core/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 := "\"v2ray.com\""
+	var address Address
+	common.Must(json.Unmarshal([]byte(rawJson), &address))
+	if address.Domain() != "v2ray.com" {
+		t.Error("domain: ", 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@v2ray.com",
+    "level": 1,
+    "alterId": 100
+  }`), user))
+
+	nUser := user.Build()
+	if r := cmp.Diff(nUser, &protocol.User{
+		Level: 1,
+		Email: "love@v2ray.com",
+	}); 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 - 0
infra/conf/conf.go

@@ -0,0 +1,3 @@
+package conf
+
+//go:generate errorgen

+ 176 - 0
infra/conf/dns.go

@@ -0,0 +1,176 @@
+package conf
+
+import (
+	"encoding/json"
+	"sort"
+	"strings"
+
+	"v2ray.com/core/app/dns"
+	"v2ray.com/core/app/router"
+	"v2ray.com/core/common/net"
+)
+
+type NameServerConfig struct {
+	Address *Address
+	Port    uint16
+	Domains []string
+}
+
+func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
+	var address Address
+	if err := json.Unmarshal(data, &address); err == nil {
+		c.Address = &address
+		c.Port = 53
+		return nil
+	}
+
+	var advanced struct {
+		Address *Address `json:"address"`
+		Port    uint16   `json:"port"`
+		Domains []string `json:"domains"`
+	}
+	if err := json.Unmarshal(data, &advanced); err == nil {
+		c.Address = advanced.Address
+		c.Port = advanced.Port
+		c.Domains = advanced.Domains
+		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
+
+	for _, d := range c.Domains {
+		parsedDomain, err := parseDomainRule(d)
+		if err != nil {
+			return nil, newError("invalid domain rule: ", d).Base(err)
+		}
+
+		for _, pd := range parsedDomain {
+			domains = append(domains, &dns.NameServer_PriorityDomain{
+				Type:   toDomainMatchingType(pd.Type),
+				Domain: pd.Value,
+			})
+		}
+	}
+
+	return &dns.NameServer{
+		Address: &net.Endpoint{
+			Network: net.Network_UDP,
+			Address: c.Address.Build(),
+			Port:    uint32(c.Port),
+		},
+		PrioritizedDomain: domains,
+	}, 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
+			if strings.HasPrefix(domain, "domain:") {
+				mapping := getHostMapping(addr)
+				mapping.Type = dns.DomainMatchingType_Subdomain
+				mapping.Domain = domain[7:]
+
+				mappings = append(mappings, mapping)
+			} else if 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)
+				}
+			} else {
+				mapping := getHostMapping(addr)
+				mapping.Type = dns.DomainMatchingType_Full
+				mapping.Domain = domain
+
+				mappings = append(mappings, mapping)
+			}
+
+			config.StaticHosts = append(config.StaticHosts, mappings...)
+		}
+	}
+
+	return config, nil
+}

+ 12 - 0
infra/conf/dns_proxy.go

@@ -0,0 +1,12 @@
+package conf
+
+import (
+	"github.com/golang/protobuf/proto"
+	"v2ray.com/core/proxy/dns"
+)
+
+type DnsOutboundConfig struct{}
+
+func (c *DnsOutboundConfig) Build() (proto.Message, error) {
+	return new(dns.Config), nil
+}

+ 103 - 0
infra/conf/dns_test.go

@@ -0,0 +1,103 @@
+package conf_test
+
+import (
+	"encoding/json"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+	"v2ray.com/core/app/dns"
+	"v2ray.com/core/common"
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/common/platform"
+	"v2ray.com/core/common/platform/filesystem"
+	. "v2ray.com/core/infra/conf"
+)
+
+func init() {
+	wd, err := os.Getwd()
+	common.Must(err)
+
+	common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoip.dat"), filepath.Join(wd, "..", "..", "release", "config", "geoip.dat")))
+	common.Must(filesystem.CopyFile(platform.GetAssetLocation("geosite.dat"), filepath.Join(wd, "..", "..", "release", "config", "geosite.dat")))
+}
+func TestDnsConfigParsing(t *testing.T) {
+	geositePath := platform.GetAssetLocation("geosite.dat")
+	defer func() {
+		os.Remove(geositePath)
+	}()
+
+	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:v2ray.com"]
+				}],
+				"hosts": {
+					"v2ray.com": "127.0.0.1",
+					"geosite:tld-cn": "10.0.0.1",
+					"domain:example.com": "google.com"
+				},
+				"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: "v2ray.com",
+							},
+						},
+					},
+				},
+				StaticHosts: []*dns.Config_HostMapping{
+					{
+						Type:          dns.DomainMatchingType_Subdomain,
+						Domain:        "example.com",
+						ProxiedDomain: "google.com",
+					},
+					{
+						Type:   dns.DomainMatchingType_Subdomain,
+						Domain: "cn",
+						Ip:     [][]byte{{10, 0, 0, 1}},
+					},
+					{
+						Type:   dns.DomainMatchingType_Subdomain,
+						Domain: "xn--fiqs8s",
+						Ip:     [][]byte{{10, 0, 0, 1}},
+					},
+					{
+						Type:   dns.DomainMatchingType_Full,
+						Domain: "v2ray.com",
+						Ip:     [][]byte{{127, 0, 0, 1}},
+					},
+				},
+				ClientIp: []byte{10, 0, 0, 1},
+			},
+		},
+	})
+}

+ 28 - 0
infra/conf/dokodemo.go

@@ -0,0 +1,28 @@
+package conf
+
+import (
+	"github.com/golang/protobuf/proto"
+	"v2ray.com/core/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 - 0
infra/conf/dokodemo_test.go

@@ -0,0 +1,41 @@
+package conf_test
+
+import (
+	"testing"
+
+	"v2ray.com/core/common/net"
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/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 - 0
infra/conf/errors.generated.go

@@ -0,0 +1,9 @@
+package conf
+
+import "v2ray.com/core/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 57 - 0
infra/conf/freedom.go

@@ -0,0 +1,57 @@
+package conf
+
+import (
+	"net"
+	"strings"
+
+	"github.com/golang/protobuf/proto"
+	v2net "v2ray.com/core/common/net"
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/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
+	}
+	config.Timeout = 600
+	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 - 0
infra/conf/freedom_test.go

@@ -0,0 +1,43 @@
+package conf_test
+
+import (
+	"testing"
+
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/common/protocol"
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/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 - 0
infra/conf/general_test.go

@@ -0,0 +1,36 @@
+package conf_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+	"v2ray.com/core/common"
+	. "v2ray.com/core/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)
+		}
+	}
+}

+ 35 - 0
infra/conf/http.go

@@ -0,0 +1,35 @@
+package conf
+
+import (
+	"github.com/golang/protobuf/proto"
+	"v2ray.com/core/proxy/http"
+)
+
+type HttpAccount struct {
+	Username string `json:"user"`
+	Password string `json:"pass"`
+}
+
+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
+}

+ 39 - 0
infra/conf/http_test.go

@@ -0,0 +1,39 @@
+package conf_test
+
+import (
+	"testing"
+
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/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 - 0
infra/conf/json/reader.go

@@ -0,0 +1,133 @@
+package json
+
+import (
+	"io"
+
+	"v2ray.com/core/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
+}

+ 97 - 0
infra/conf/json/reader_test.go

@@ -0,0 +1,97 @@
+package json_test
+
+import (
+	"bytes"
+	"io"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"v2ray.com/core/common"
+	. "v2ray.com/core/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 - 0
infra/conf/loader.go

@@ -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
+}

+ 57 - 0
infra/conf/log.go

@@ -0,0 +1,57 @@
+package conf
+
+import (
+	"strings"
+
+	"v2ray.com/core/app/log"
+	clog "v2ray.com/core/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 len(v.AccessLog) > 0 {
+		config.AccessLogPath = v.AccessLog
+		config.AccessLogType = log.LogType_File
+	}
+	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 - 0
infra/conf/mtproto.go

@@ -0,0 +1,69 @@
+package conf
+
+import (
+	"encoding/hex"
+	"encoding/json"
+
+	"github.com/golang/protobuf/proto"
+
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	"v2ray.com/core/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 - 0
infra/conf/mtproto_test.go

@@ -0,0 +1,40 @@
+package conf_test
+
+import (
+	"testing"
+
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/proxy/mtproto"
+)
+
+func TestMTProtoServerConfig(t *testing.T) {
+	creator := func() Buildable {
+		return new(MTProtoServerConfig)
+	}
+
+	runMultiTestCase(t, []TestCase{
+		{
+			Input: `{
+				"users": [{
+					"email": "love@v2ray.com",
+					"level": 1,
+					"secret": "b0cbcef5a486d9636472ac27f8e11a9d"
+				}]
+			}`,
+			Parser: loadJSON(creator),
+			Output: &mtproto.ServerConfig{
+				User: []*protocol.User{
+					{
+						Email: "love@v2ray.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},
+						}),
+					},
+				},
+			},
+		},
+	})
+}

+ 96 - 0
infra/conf/policy.go

@@ -0,0 +1,96 @@
+package conf
+
+import (
+	"v2ray.com/core/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"`
+}
+
+func (p *SystemPolicy) Build() (*policy.SystemPolicy, error) {
+	return &policy.SystemPolicy{
+		Stats: &policy.SystemPolicy_Stats{
+			InboundUplink:   p.StatsInboundUplink,
+			InboundDownlink: p.StatsInboundDownlink,
+		},
+	}, 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 - 0
infra/conf/policy_test.go

@@ -0,0 +1,40 @@
+package conf_test
+
+import (
+	"testing"
+
+	"v2ray.com/core/common"
+	. "v2ray.com/core/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 := int32(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 - 0
infra/conf/reverse.go

@@ -0,0 +1,56 @@
+package conf
+
+import (
+	"github.com/golang/protobuf/proto"
+	"v2ray.com/core/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 - 0
infra/conf/reverse_test.go

@@ -0,0 +1,45 @@
+package conf_test
+
+import (
+	"testing"
+
+	"v2ray.com/core/app/reverse"
+	"v2ray.com/core/infra/conf"
+)
+
+func TestReverseConfig(t *testing.T) {
+	creator := func() conf.Buildable {
+		return new(conf.ReverseConfig)
+	}
+
+	runMultiTestCase(t, []TestCase{
+		{
+			Input: `{
+				"bridges": [{
+					"tag": "test",
+					"domain": "test.v2ray.com"
+				}]
+			}`,
+			Parser: loadJSON(creator),
+			Output: &reverse.Config{
+				BridgeConfig: []*reverse.BridgeConfig{
+					{Tag: "test", Domain: "test.v2ray.com"},
+				},
+			},
+		},
+		{
+			Input: `{
+				"portals": [{
+					"tag": "test",
+					"domain": "test.v2ray.com"
+				}]
+			}`,
+			Parser: loadJSON(creator),
+			Output: &reverse.Config{
+				PortalConfig: []*reverse.PortalConfig{
+					{Tag: "test", Domain: "test.v2ray.com"},
+				},
+			},
+		},
+	})
+}

+ 502 - 0
infra/conf/router.go

@@ -0,0 +1,502 @@
+package conf
+
+import (
+	"encoding/json"
+	"strconv"
+	"strings"
+
+	"v2ray.com/core/app/router"
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/common/platform/filesystem"
+
+	"github.com/golang/protobuf/proto"
+)
+
+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 len(r.Tag) == 0 {
+		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()
+
+	rawRuleList := c.RuleList
+	if c.Settings != nil {
+		rawRuleList = append(c.RuleList, c.Settings.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: " + 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("country not found: " + 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
+	}
+
+	if 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 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:]
+	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
+		}
+
+		if strings.HasPrefix(ip, "ext:") {
+			kv := strings.Split(ip[4:], ":")
+			if len(kv) != 2 {
+				return nil, newError("invalid external resource: ", ip)
+			}
+
+			filename := kv[0]
+			country := kv[1]
+			geoip, err := loadGeoIP(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       *PortRange   `json:"port"`
+		Network    *NetworkList `json:"network"`
+		SourceIP   *StringList  `json:"source"`
+		User       *StringList  `json:"user"`
+		InboundTag *StringList  `json:"inboundTag"`
+		Protocols  *StringList  `json:"protocol"`
+	}
+	rawFieldRule := new(RawFieldRule)
+	err := json.Unmarshal(msg, rawFieldRule)
+	if err != nil {
+		return nil, err
+	}
+
+	rule := new(router.RoutingRule)
+	if len(rawFieldRule.OutboundTag) > 0 {
+		rule.TargetTag = &router.RoutingRule_Tag{
+			Tag: rawFieldRule.OutboundTag,
+		}
+	} else if len(rawFieldRule.BalancerTag) > 0 {
+		rule.TargetTag = &router.RoutingRule_BalancingTag{
+			BalancingTag: rawFieldRule.BalancerTag,
+		}
+	} else {
+		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.PortRange = 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.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)
+		}
+	}
+
+	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
+}

+ 233 - 0
infra/conf/router_test.go

@@ -0,0 +1,233 @@
+package conf_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+
+	"v2ray.com/core/app/router"
+	. "v2ray.com/core/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"
+						}
+					]
+				},
+				"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",
+						},
+					},
+				},
+			},
+		},
+		{
+			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 - 0
infra/conf/serial/errors.generated.go

@@ -0,0 +1,9 @@
+package serial
+
+import "v2ray.com/core/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 71 - 0
infra/conf/serial/loader.go

@@ -0,0 +1,71 @@
+package serial
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+
+	"v2ray.com/core"
+	"v2ray.com/core/common/errors"
+	"v2ray.com/core/infra/conf"
+	json_reader "v2ray.com/core/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}
+}
+
+func LoadJSONConfig(reader io.Reader) (*core.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)
+	}
+
+	pbConfig, err := jsonConfig.Build()
+	if err != nil {
+		return nil, newError("failed to parse json config").Base(err)
+	}
+
+	return pbConfig, nil
+}

+ 63 - 0
infra/conf/serial/loader_test.go

@@ -0,0 +1,63 @@
+package serial_test
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"v2ray.com/core/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 - 0
infra/conf/serial/serial.go

@@ -0,0 +1,3 @@
+package serial
+
+//go:generate errorgen

+ 139 - 0
infra/conf/shadowsocks.go

@@ -0,0 +1,139 @@
+package conf
+
+import (
+	"strings"
+
+	"github.com/golang/protobuf/proto"
+
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	"v2ray.com/core/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
+	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"`
+	OTA         *bool        `json:"ota"`
+	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 len(v.Password) == 0 {
+		return nil, newError("Shadowsocks password is not specified.")
+	}
+	account := &shadowsocks.Account{
+		Password: v.Password,
+		Ota:      shadowsocks.Account_Auto,
+	}
+	if v.OTA != nil {
+		if *v.OTA {
+			account.Ota = shadowsocks.Account_Enabled
+		} else {
+			account.Ota = shadowsocks.Account_Disabled
+		}
+	}
+	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 len(server.Password) == 0 {
+			return nil, newError("Shadowsocks password is not specified.")
+		}
+		account := &shadowsocks.Account{
+			Password: server.Password,
+			Ota:      shadowsocks.Account_Enabled,
+		}
+		if !server.Ota {
+			account.Ota = shadowsocks.Account_Disabled
+		}
+		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 - 0
infra/conf/shadowsocks_test.go

@@ -0,0 +1,36 @@
+package conf_test
+
+import (
+	"testing"
+
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/proxy/shadowsocks"
+)
+
+func TestShadowsocksServerConfigParsing(t *testing.T) {
+	creator := func() Buildable {
+		return new(ShadowsocksServerConfig)
+	}
+
+	runMultiTestCase(t, []TestCase{
+		{
+			Input: `{
+				"method": "aes-128-cfb",
+				"password": "v2ray-password"
+			}`,
+			Parser: loadJSON(creator),
+			Output: &shadowsocks.ServerConfig{
+				User: &protocol.User{
+					Account: serial.ToTypedMessage(&shadowsocks.Account{
+						CipherType: shadowsocks.CipherType_AES_128_CFB,
+						Password:   "v2ray-password",
+					}),
+				},
+				Network: []net.Network{net.Network_TCP},
+			},
+		},
+	})
+}

+ 99 - 0
infra/conf/socks.go

@@ -0,0 +1,99 @@
+package conf
+
+import (
+	"encoding/json"
+
+	"github.com/golang/protobuf/proto"
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	"v2ray.com/core/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 - 0
infra/conf/socks_test.go

@@ -0,0 +1,92 @@
+package conf_test
+
+import (
+	"testing"
+
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/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 - 0
infra/conf/transport.go

@@ -0,0 +1,89 @@
+package conf
+
+import (
+	"v2ray.com/core/common/serial"
+	"v2ray.com/core/transport"
+	"v2ray.com/core/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 - 0
infra/conf/transport_authenticators.go

@@ -0,0 +1,223 @@
+package conf
+
+import (
+	"sort"
+
+	"github.com/golang/protobuf/proto"
+
+	"v2ray.com/core/transport/internet/headers/http"
+	"v2ray.com/core/transport/internet/headers/noop"
+	"v2ray.com/core/transport/internet/headers/srtp"
+	"v2ray.com/core/transport/internet/headers/tls"
+	"v2ray.com/core/transport/internet/headers/utp"
+	"v2ray.com/core/transport/internet/headers/wechat"
+	"v2ray.com/core/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 HTTPAuthenticatorRequest 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 *HTTPAuthenticatorRequest) 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 HTTPAuthenticatorResponse struct {
+	Version string                 `json:"version"`
+	Status  string                 `json:"status"`
+	Reason  string                 `json:"reason"`
+	Headers map[string]*StringList `json:"headers"`
+}
+
+func (v *HTTPAuthenticatorResponse) 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 HTTPAuthenticator struct {
+	Request  HTTPAuthenticatorRequest  `json:"request"`
+	Response HTTPAuthenticatorResponse `json:"response"`
+}
+
+func (v *HTTPAuthenticator) 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
+}

+ 476 - 0
infra/conf/transport_internet.go

@@ -0,0 +1,476 @@
+package conf
+
+import (
+	"encoding/json"
+	"strings"
+
+	"github.com/golang/protobuf/proto"
+	"v2ray.com/core/common/platform/filesystem"
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	"v2ray.com/core/transport/internet"
+	"v2ray.com/core/transport/internet/domainsocket"
+	"v2ray.com/core/transport/internet/http"
+	"v2ray.com/core/transport/internet/kcp"
+	"v2ray.com/core/transport/internet/quic"
+	"v2ray.com/core/transport/internet/tcp"
+	"v2ray.com/core/transport/internet/tls"
+	"v2ray.com/core/transport/internet/websocket"
+)
+
+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(HTTPAuthenticator) },
+	}, "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"`
+}
+
+// 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)
+	}
+
+	return config, nil
+}
+
+type TCPConfig struct {
+	HeaderConfig json.RawMessage `json:"header"`
+}
+
+// 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)
+	}
+
+	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"`
+}
+
+// Build implements Buildable.
+func (c *WebSocketConfig) Build() (proto.Message, error) {
+	path := c.Path
+	if len(path) == 0 && len(c.Path2) > 0 {
+		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,
+	}
+	return config, nil
+}
+
+type HTTPConfig struct {
+	Host *StringList `json:"host"`
+	Path string      `json:"path"`
+}
+
+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"`
+}
+
+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"`
+}
+
+func (c *DomainSocketConfig) Build() (proto.Message, error) {
+	return &domainsocket.Config{
+		Path:     c.Path,
+		Abstract: c.Abstract,
+	}, nil
+}
+
+type TLSCertConfig struct {
+	CertFile string   `json:"certificateFile"`
+	CertStr  []string `json:"certificate"`
+	KeyFile  string   `json:"keyFile"`
+	KeyStr   []string `json:"key"`
+	Usage    string   `json:"usage"`
+}
+
+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.")
+}
+
+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"`
+}
+
+// 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)
+	}
+	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"`
+}
+
+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,
+	}, nil
+}
+
+type StreamConfig struct {
+	Network        *TransportProtocol  `json:"network"`
+	Security       string              `json:"security"`
+	TLSSettings    *TLSConfig          `json:"tlsSettings"`
+	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.ToLower(c.Security) == "tls" {
+		tlsSettings := c.TLSSettings
+		if tlsSettings == nil {
+			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 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 len(v.Tag) == 0 {
+		return nil, newError("Proxy tag is not set.")
+	}
+	return &internet.ProxyConfig{
+		Tag: v.Tag,
+	}, nil
+}

+ 169 - 0
infra/conf/transport_test.go

@@ -0,0 +1,169 @@
+package conf_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/transport"
+	"v2ray.com/core/transport/internet"
+	"v2ray.com/core/transport/internet/headers/http"
+	"v2ray.com/core/transport/internet/headers/noop"
+	"v2ray.com/core/transport/internet/headers/tls"
+	"v2ray.com/core/transport/internet/kcp"
+	"v2ray.com/core/transport/internet/quic"
+	"v2ray.com/core/transport/internet/tcp"
+	"v2ray.com/core/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{}),
+						}),
+					},
+				},
+			},
+		},
+	})
+}

+ 446 - 0
infra/conf/v2ray.go

@@ -0,0 +1,446 @@
+package conf
+
+import (
+	"encoding/json"
+	"strings"
+
+	"v2ray.com/core"
+	"v2ray.com/core/app/dispatcher"
+	"v2ray.com/core/app/proxyman"
+	"v2ray.com/core/app/stats"
+	"v2ray.com/core/common/serial"
+)
+
+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) },
+		"vmess":         func() interface{} { return new(VMessInboundConfig) },
+		"mtproto":       func() interface{} { return new(MTProtoServerConfig) },
+	}, "protocol", "settings")
+
+	outboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{
+		"blackhole":   func() interface{} { return new(BlackholeConfig) },
+		"freedom":     func() interface{} { return new(FreedomConfig) },
+		"shadowsocks": func() interface{} { return new(ShadowsocksClientConfig) },
+		"vmess":       func() interface{} { return new(VMessOutboundConfig) },
+		"socks":       func() interface{} { return new(SocksClientConfig) },
+		"mtproto":     func() interface{} { return new(MTProtoClientConfig) },
+		"dns":         func() interface{} { return new(DnsOutboundConfig) },
+	}, "protocol", "settings")
+)
+
+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"`
+}
+
+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 uint16 `json:"concurrency"`
+}
+
+func (c *MuxConfig) GetConcurrency() uint16 {
+	if c.Concurrency == 0 {
+		return 8
+	}
+	return c.Concurrency
+}
+
+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.PortRange == nil {
+		return nil, newError("port range not specified in InboundDetour.")
+	}
+	receiverSettings.PortRange = c.PortRange.Build()
+
+	if c.ListenOn != nil {
+		if c.ListenOn.Family().IsDomain() {
+			return nil, newError("unable to listen on domain address: ", c.ListenOn.Domain())
+		}
+		receiverSettings.Listen = c.ListenOn.Build()
+	}
+	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
+		}
+		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
+		}
+		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 && c.MuxSettings.Enabled {
+		senderSettings.MultiplexSettings = &proxyman.MultiplexingConfig{
+			Enabled:     true,
+			Concurrency: uint32(c.MuxSettings.GetConcurrency()),
+		}
+	}
+
+	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{}
+
+func (c *StatsConfig) Build() (*stats.Config, error) {
+	return &stats.Config{}, nil
+}
+
+type Config struct {
+	Port            uint16                 `json:"port"` // Port of this Point server. Deprecated.
+	LogConfig       *LogConfig             `json:"log"`
+	RouterConfig    *RouterConfig          `json:"routing"`
+	DNSConfig       *DnsConfig             `json:"dns"`
+	InboundConfigs  []InboundDetourConfig  `json:"inbounds"`
+	OutboundConfigs []OutboundDetourConfig `json:"outbounds"`
+	InboundConfig   *InboundDetourConfig   `json:"inbound"`        // Deprecated.
+	OutboundConfig  *OutboundDetourConfig  `json:"outbound"`       // Deprecated.
+	InboundDetours  []InboundDetourConfig  `json:"inboundDetour"`  // Deprecated.
+	OutboundDetours []OutboundDetourConfig `json:"outboundDetour"` // Deprecated.
+	Transport       *TransportConfig       `json:"transport"`
+	Policy          *PolicyConfig          `json:"policy"`
+	Api             *ApiConfig             `json:"api"`
+	Stats           *StatsConfig           `json:"stats"`
+	Reverse         *ReverseConfig         `json:"reverse"`
+}
+
+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))
+	}
+
+	if c.LogConfig != nil {
+		config.App = append(config.App, serial.ToTypedMessage(c.LogConfig.Build()))
+	} else {
+		config.App = append(config.App, serial.ToTypedMessage(DefaultLogConfig()))
+	}
+
+	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
+}

+ 338 - 0
infra/conf/v2ray_test.go

@@ -0,0 +1,338 @@
+package conf_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+
+	"v2ray.com/core"
+	"v2ray.com/core/app/dispatcher"
+	"v2ray.com/core/app/log"
+	"v2ray.com/core/app/proxyman"
+	"v2ray.com/core/app/router"
+	clog "v2ray.com/core/common/log"
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/proxy/blackhole"
+	dns_proxy "v2ray.com/core/proxy/dns"
+	"v2ray.com/core/proxy/freedom"
+	"v2ray.com/core/proxy/vmess"
+	"v2ray.com/core/proxy/vmess/inbound"
+	"v2ray.com/core/transport/internet"
+	"v2ray.com/core/transport/internet/http"
+	"v2ray.com/core/transport/internet/tls"
+	"v2ray.com/core/transport/internet/websocket"
+)
+
+func TestV2RayConfig(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/v2ray/access.log",
+					"loglevel": "error",
+					"error": "/var/log/v2ray/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(&dispatcher.Config{}),
+					serial.ToTypedMessage(&proxyman.InboundConfig{}),
+					serial.ToTypedMessage(&proxyman.OutboundConfig{}),
+					serial.ToTypedMessage(&log.Config{
+						ErrorLogType:  log.LogType_File,
+						ErrorLogPath:  "/var/log/v2ray/error.log",
+						ErrorLogLevel: clog.Severity_Error,
+						AccessLogType: log.LogType_File,
+						AccessLogPath: "/var/log/v2ray/access.log",
+					}),
+					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,
+							Timeout:        600,
+							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{}),
+					},
+				},
+				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: "v2ray.core.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: "v2ray.core.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,
+										},
+									}),
+								},
+							},
+						}),
+					},
+				},
+			},
+		},
+	})
+}

+ 164 - 0
infra/conf/vmess.go

@@ -0,0 +1,164 @@
+package conf
+
+import (
+	"encoding/json"
+	"strings"
+
+	"github.com/golang/protobuf/proto"
+
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	"v2ray.com/core/proxy/vmess"
+	"v2ray.com/core/proxy/vmess/inbound"
+	"v2ray.com/core/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)
+	if config.AlterId == 0 {
+		config.AlterId = 32
+	}
+	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"`
+}
+
+var bUser = "a06fe789-5ab1-480b-8124-ae4599801ff3"
+
+// 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 - 0
infra/conf/vmess_test.go

@@ -0,0 +1,117 @@
+package conf_test
+
+import (
+	"testing"
+
+	"v2ray.com/core/common/net"
+	"v2ray.com/core/common/protocol"
+	"v2ray.com/core/common/serial"
+	. "v2ray.com/core/infra/conf"
+	"v2ray.com/core/proxy/vmess"
+	"v2ray.com/core/proxy/vmess/inbound"
+	"v2ray.com/core/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@v2ray.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@v2ray.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@v2ray.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@v2ray.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,
+			},
+		},
+	})
+}

+ 144 - 0
infra/control/api.go

@@ -0,0 +1,144 @@
+package control
+
+import (
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"strings"
+
+	"github.com/golang/protobuf/proto"
+	"google.golang.org/grpc"
+
+	logService "v2ray.com/core/app/log/command"
+	statsService "v2ray.com/core/app/stats/command"
+	"v2ray.com/core/common"
+)
+
+type ApiCommand struct{}
+
+func (c *ApiCommand) Name() string {
+	return "api"
+}
+
+func (c *ApiCommand) Description() Description {
+	return Description{
+		Short: "Call V2Ray API",
+		Usage: []string{
+			"v2ctl api [--server=127.0.0.1:8080] Service.Method Request",
+			"Call an API in an V2Ray process.",
+			"The following methods are currently supported:",
+			"\tLoggerService.RestartLogger",
+			"\tStatsService.GetStats",
+			"\tStatsService.QueryStats",
+			"Examples:",
+			"v2ctl api --server=127.0.0.1:8080 LoggerService.RestartLogger '' ",
+			"v2ctl api --server=127.0.0.1:8080 StatsService.QueryStats 'pattern: \"\" reset: false'",
+			"v2ctl api --server=127.0.0.1:8080 StatsService.GetStats 'name: \"inbound>>>statin>>>traffic>>>downlink\" reset: false'",
+		},
+	}
+}
+
+func (c *ApiCommand) Execute(args []string) error {
+	fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+
+	serverAddrPtr := fs.String("server", "127.0.0.1:8080", "Server address")
+
+	if err := fs.Parse(args); err != nil {
+		return err
+	}
+
+	conn, err := grpc.Dial(*serverAddrPtr, grpc.WithInsecure(), grpc.WithBlock())
+	if err != nil {
+		return newError("failed to dial ", *serverAddrPtr).Base(err)
+	}
+	defer conn.Close()
+
+	unnamedArgs := fs.Args()
+	if len(unnamedArgs) < 2 {
+		return newError("service name or request not specified.")
+	}
+
+	service, method := getServiceMethod(unnamedArgs[0])
+	handler, found := serivceHandlerMap[strings.ToLower(service)]
+	if !found {
+		return newError("unknown service: ", service)
+	}
+
+	response, err := handler(conn, method, unnamedArgs[1])
+	if err != nil {
+		return newError("failed to call service ", unnamedArgs[0]).Base(err)
+	}
+
+	fmt.Println(response)
+	return nil
+}
+
+func getServiceMethod(s string) (string, string) {
+	ss := strings.Split(s, ".")
+	service := ss[0]
+	var method string
+	if len(ss) > 1 {
+		method = ss[1]
+	}
+	return service, method
+}
+
+type serviceHandler func(conn *grpc.ClientConn, method string, request string) (string, error)
+
+var serivceHandlerMap = map[string]serviceHandler{
+	"statsservice":  callStatsService,
+	"loggerservice": callLogService,
+}
+
+func callLogService(conn *grpc.ClientConn, method string, request string) (string, error) {
+	client := logService.NewLoggerServiceClient(conn)
+
+	switch strings.ToLower(method) {
+	case "restartlogger":
+		r := &logService.RestartLoggerRequest{}
+		if err := proto.UnmarshalText(request, r); err != nil {
+			return "", err
+		}
+		resp, err := client.RestartLogger(context.Background(), r)
+		if err != nil {
+			return "", err
+		}
+		return proto.MarshalTextString(resp), nil
+	default:
+		return "", errors.New("Unknown method: " + method)
+	}
+}
+
+func callStatsService(conn *grpc.ClientConn, method string, request string) (string, error) {
+	client := statsService.NewStatsServiceClient(conn)
+
+	switch strings.ToLower(method) {
+	case "getstats":
+		r := &statsService.GetStatsRequest{}
+		if err := proto.UnmarshalText(request, r); err != nil {
+			return "", err
+		}
+		resp, err := client.GetStats(context.Background(), r)
+		if err != nil {
+			return "", err
+		}
+		return proto.MarshalTextString(resp), nil
+	case "querystats":
+		r := &statsService.QueryStatsRequest{}
+		if err := proto.UnmarshalText(request, r); err != nil {
+			return "", err
+		}
+		resp, err := client.QueryStats(context.Background(), r)
+		if err != nil {
+			return "", err
+		}
+		return proto.MarshalTextString(resp), nil
+	default:
+		return "", errors.New("Unknown method: " + method)
+	}
+}
+
+func init() {
+	common.Must(RegisterCommand(&ApiCommand{}))
+}

+ 139 - 0
infra/control/cert.go

@@ -0,0 +1,139 @@
+package control
+
+import (
+	"context"
+	"crypto/x509"
+	"encoding/json"
+	"flag"
+	"os"
+	"strings"
+	"time"
+
+	"v2ray.com/core/common"
+	"v2ray.com/core/common/protocol/tls/cert"
+	"v2ray.com/core/common/task"
+)
+
+type stringList []string
+
+func (l *stringList) String() string {
+	return "String list"
+}
+
+func (l *stringList) Set(v string) error {
+	if len(v) == 0 {
+		return newError("empty value")
+	}
+	*l = append(*l, v)
+	return nil
+}
+
+type jsonCert struct {
+	Certificate []string `json:"certificate"`
+	Key         []string `json:"key"`
+}
+
+type CertificateCommand struct {
+}
+
+func (c *CertificateCommand) Name() string {
+	return "cert"
+}
+
+func (c *CertificateCommand) Description() Description {
+	return Description{
+		Short: "Generate TLS certificates.",
+		Usage: []string{
+			"v2ctl cert [--ca] [--domain=v2ray.com] [--expire=240h]",
+			"Generate new TLS certificate",
+			"--ca The new certificate is a CA certificate",
+			"--domain Common name for the certificate",
+			"--exipre Time until certificate expires. 240h = 10 days.",
+		},
+	}
+}
+
+func (c *CertificateCommand) printJson(certificate *cert.Certificate) {
+	certPEM, keyPEM := certificate.ToPEM()
+	jCert := &jsonCert{
+		Certificate: strings.Split(strings.TrimSpace(string(certPEM)), "\n"),
+		Key:         strings.Split(strings.TrimSpace(string(keyPEM)), "\n"),
+	}
+	content, err := json.MarshalIndent(jCert, "", "  ")
+	common.Must(err)
+	os.Stdout.Write(content)
+	os.Stdout.WriteString("\n")
+}
+
+func (c *CertificateCommand) writeFile(content []byte, name string) error {
+	f, err := os.Create(name)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	return common.Error2(f.Write(content))
+}
+
+func (c *CertificateCommand) printFile(certificate *cert.Certificate, name string) error {
+	certPEM, keyPEM := certificate.ToPEM()
+	return task.Run(context.Background(), func() error {
+		return c.writeFile(certPEM, name+"_cert.pem")
+	}, func() error {
+		return c.writeFile(keyPEM, name+"_key.pem")
+	})
+}
+
+func (c *CertificateCommand) Execute(args []string) error {
+	fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+
+	var domainNames stringList
+	fs.Var(&domainNames, "domain", "Domain name for the certificate")
+
+	commonName := fs.String("name", "V2Ray Inc", "The common name of this certificate")
+	organization := fs.String("org", "V2Ray Inc", "Organization of the certificate")
+
+	isCA := fs.Bool("ca", false, "Whether this certificate is a CA")
+	jsonOutput := fs.Bool("json", true, "Print certificate in JSON format")
+	fileOutput := fs.String("file", "", "Save certificate in file.")
+
+	expire := fs.Duration("expire", time.Hour*24*90 /* 90 days */, "Time until the certificate expires. Default value 3 months.")
+
+	if err := fs.Parse(args); err != nil {
+		return err
+	}
+
+	var opts []cert.Option
+	if *isCA {
+		opts = append(opts, cert.Authority(*isCA))
+		opts = append(opts, cert.KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature))
+	}
+
+	opts = append(opts, cert.NotAfter(time.Now().Add(*expire)))
+	opts = append(opts, cert.CommonName(*commonName))
+	if len(domainNames) > 0 {
+		opts = append(opts, cert.DNSNames(domainNames...))
+	}
+	opts = append(opts, cert.Organization(*organization))
+
+	cert, err := cert.Generate(nil, opts...)
+	if err != nil {
+		return newError("failed to generate TLS certificate").Base(err)
+	}
+
+	if *jsonOutput {
+		c.printJson(cert)
+	}
+
+	if len(*fileOutput) > 0 {
+		if err := c.printFile(cert, *fileOutput); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func init() {
+	common.Must(RegisterCommand(&CertificateCommand{}))
+}

+ 51 - 0
infra/control/command.go

@@ -0,0 +1,51 @@
+package control
+
+import (
+	"fmt"
+	"strings"
+)
+
+type Description struct {
+	Short string
+	Usage []string
+}
+
+type Command interface {
+	Name() string
+	Description() Description
+	Execute(args []string) error
+}
+
+var (
+	commandRegistry = make(map[string]Command)
+)
+
+func RegisterCommand(cmd Command) error {
+	entry := strings.ToLower(cmd.Name())
+	if len(entry) == 0 {
+		return newError("empty command name")
+	}
+	commandRegistry[entry] = cmd
+	return nil
+}
+
+func GetCommand(name string) Command {
+	cmd, found := commandRegistry[name]
+	if !found {
+		return nil
+	}
+	return cmd
+}
+
+type hiddenCommand interface {
+	Hidden() bool
+}
+
+func PrintUsage() {
+	for name, cmd := range commandRegistry {
+		if _, ok := cmd.(hiddenCommand); ok {
+			continue
+		}
+		fmt.Println("   ", name, "\t\t\t", cmd.Description())
+	}
+}

+ 3 - 0
infra/control/control.go

@@ -0,0 +1,3 @@
+package control
+
+//go:generate errorgen

+ 9 - 0
infra/control/errors.generated.go

@@ -0,0 +1,9 @@
+package control
+
+import "v2ray.com/core/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 70 - 0
infra/control/fetch.go

@@ -0,0 +1,70 @@
+package control
+
+import (
+	"net/http"
+	"net/url"
+	"os"
+	"strings"
+
+	"v2ray.com/core/common"
+	"v2ray.com/core/common/buf"
+)
+
+type FetchCommand struct{}
+
+func (c *FetchCommand) Name() string {
+	return "fetch"
+}
+
+func (c *FetchCommand) Description() Description {
+	return Description{
+		Short: "Fetch resources",
+		Usage: []string{"v2ctl fetch <url>"},
+	}
+}
+
+func (c *FetchCommand) isValidScheme(scheme string) bool {
+	scheme = strings.ToLower(scheme)
+	return scheme == "http" || scheme == "https"
+}
+
+func (c *FetchCommand) Execute(args []string) error {
+	if len(args) < 1 {
+		return newError("empty url")
+	}
+	target := args[0]
+	parsedTarget, err := url.Parse(target)
+	if err != nil {
+		return newError("invalid URL: ", target).Base(err)
+	}
+	if !c.isValidScheme(parsedTarget.Scheme) {
+		return newError("invalid scheme: ", parsedTarget.Scheme)
+	}
+
+	client := &http.Client{}
+	resp, err := client.Do(&http.Request{
+		Method: "GET",
+		URL:    parsedTarget,
+		Close:  true,
+	})
+	if err != nil {
+		return newError("failed to dial to ", target).Base(err)
+	}
+
+	if resp.StatusCode != 200 {
+		return newError("unexpected HTTP status code: ", resp.StatusCode)
+	}
+
+	content, err := buf.ReadAllToBytes(resp.Body)
+	if err != nil {
+		return newError("failed to read HTTP response").Base(err)
+	}
+
+	os.Stdout.Write(content)
+
+	return nil
+}
+
+func init() {
+	common.Must(RegisterCommand(&FetchCommand{}))
+}

+ 53 - 0
infra/control/love.go

@@ -0,0 +1,53 @@
+package control
+
+import (
+	"bufio"
+	"bytes"
+	"compress/gzip"
+	"encoding/base64"
+	"fmt"
+
+	"v2ray.com/core/common"
+	"v2ray.com/core/common/platform"
+)
+
+const content = "H4sIAAAAAAAC/4SVMaskNwzH+/kUW6izcSthMGrcqLhVk0rdQS5cSMg7Xu4S0vizB8meZd57M3ta2GHX/ukvyZZmY2ZKDMzCzJyY5yOlxKII1omsf+qkBiiC6WhbYsbkjDAfySQsJqD3jtrD0EBM3sBHzG3kUsrglIQREXonpd47kYIi4AHmgI9Wcq2jlJITC6JZJ+v3ECYzBMAHyYm392yuY4zWsjACmHZSh6l3A0JETzGlWZqDsnArpTg62mhJONhOdO90p97V1BAnteoaOcuummtrrtuERQwUiJwP8a4KGKcyxdOCw1spOY+WHueFqmakAIgUSSuhwKNgobxKXSLbtg6r5cFmBiAeF6yCkYycmv+BiCIiW8ScHa3DgxAuZQbRhFNrLTFo96RBmx9jKWWG5nBsjyJzuIkftUblonppZU5t5LzwIks5L1a4lijagQxLokbIYwxfytNDC+XQqrWW9fzAunhqh5/Tg8PuaMw0d/Tcw3iDO81bHfWM/AnutMh2xqSUntMzd3wHDy9iHMQz8bmUZYvqedTJ5GgOnrNt7FIbSlwXE3wDI19n/KA38MsLaP4l89b5F8AV3ESOMIEhIBgezHBc0H6xV9KbaXwMvPcNvIHcC0C7UPZQx4JVTb35/AneSQq+bAYXsBmY7TCRupF2NTdVm/+ch22xa0pvRERKqt1oxj9DUbXzU84Gvj5hc5a81SlAUwMwgEs4T9+7sg9lb9h+908MWiKV8xtWciVTmnB3tivRjNerfXdxpfEBbq2NUvLMM5R9NLuyQg8nXT0PIh1xPd/wrcV49oJ6zbZaPlj2V87IY9T3F2XCOcW2MbZyZd49H+9m81E1N9SxlU+ff/1y+/f3719vf7788+Ugv/ffbMIH7ZNj0dsT4WMHHwLPu/Rp2O75uh99AK+N2xn7ZHq1OK6gczkN+9ngdOl1Qvki5xwSR8vFX6D+9vXA97B/+fr5rz9u/738uP328urP19vfP759e3n9Xs6jamvqlfJ/AAAA//+YAMZjDgkAAA=="
+
+type LoveCommand struct{}
+
+func (*LoveCommand) Name() string {
+	return "lovevictoria"
+}
+
+func (*LoveCommand) Hidden() bool {
+	return false
+}
+
+func (c *LoveCommand) Description() Description {
+	return Description{
+		Short: "",
+		Usage: []string{""},
+	}
+}
+
+func (*LoveCommand) Execute([]string) error {
+	c, err := base64.StdEncoding.DecodeString(content)
+	common.Must(err)
+	reader, err := gzip.NewReader(bytes.NewBuffer(c))
+	common.Must(err)
+	b := make([]byte, 4096)
+	nBytes, _ := reader.Read(b)
+
+	bb := bytes.NewBuffer(b[:nBytes])
+	scanner := bufio.NewScanner(bb)
+	for scanner.Scan() {
+		s := scanner.Text()
+		fmt.Print(s + platform.LineSeparator())
+	}
+
+	return nil
+}
+
+func init() {
+	common.Must(RegisterCommand(&LoveCommand{}))
+}

+ 8 - 0
infra/control/main/BUILD

@@ -0,0 +1,8 @@
+load("//bazel:build.bzl", "foreign_go_binary")
+load("//bazel:gpg.bzl", "gpg_sign")
+load("//bazel:matrix.bzl", "SUPPORTED_MATRIX")
+load("//tools/control/main:targets.bzl", "gen_targets")
+
+package(default_visibility=["//visibility:public"])
+
+gen_targets(SUPPORTED_MATRIX)

+ 48 - 0
infra/control/main/main.go

@@ -0,0 +1,48 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	_ "v2ray.com/core/infra/conf/command"
+	"v2ray.com/core/infra/control"
+)
+
+func getCommandName() string {
+	if len(os.Args) > 1 {
+		return os.Args[1]
+	}
+	return ""
+}
+
+func main() {
+	name := getCommandName()
+	cmd := control.GetCommand(name)
+	if cmd == nil {
+		fmt.Fprintln(os.Stderr, "Unknown command:", name)
+		fmt.Fprintln(os.Stderr)
+
+		fmt.Println("v2ctl <command>")
+		fmt.Println("Available commands:")
+		control.PrintUsage()
+		return
+	}
+
+	if err := cmd.Execute(os.Args[2:]); err != nil {
+		hasError := false
+		if err != flag.ErrHelp {
+			fmt.Fprintln(os.Stderr, err.Error())
+			fmt.Fprintln(os.Stderr)
+			hasError = true
+		}
+
+		for _, line := range cmd.Description().Usage {
+			fmt.Println(line)
+		}
+
+		if hasError {
+			os.Exit(-1)
+		}
+	}
+}

+ 56 - 0
infra/control/main/targets.bzl

@@ -0,0 +1,56 @@
+load("//bazel:build.bzl", "foreign_go_binary")
+load("//bazel:gpg.bzl", "gpg_sign")
+
+def gen_targets(matrix):
+  output = "v2ctl"
+  pkg = "v2ray.com/core/infra/control/main"
+
+  for (os, arch) in matrix:
+    bin_name = "v2ctl_" + os + "_" + arch
+    foreign_go_binary(
+      name = bin_name,
+      pkg = pkg,
+      output = output,
+      os = os,
+      arch = arch,
+      gotags = "confonly",
+    )
+
+    gpg_sign(
+      name = bin_name + "_sig",
+      base = ":" + bin_name,
+    )
+
+    if arch in ["mips", "mipsle"]:
+      bin_name = "v2ctl_" + os + "_" + arch + "_softfloat"
+      foreign_go_binary(
+        name = bin_name,
+        pkg = pkg,
+        output = output + "_softfloat",
+        os = os,
+        arch = arch,
+        mips = "softfloat",
+        gotags = "confonly",
+      )
+
+      gpg_sign(
+        name = bin_name + "_sig",
+        base = ":" + bin_name,
+      )
+    
+    if arch in ["arm"]:
+      bin_name = "v2ctl_" + os + "_" + arch + "_armv7"
+      foreign_go_binary(
+        name = bin_name,
+        pkg = pkg,
+        output = output + "_armv7",
+        os = os,
+        arch = arch,
+        arm = "7",
+        gotags = "confonly",
+      )
+
+      gpg_sign(
+        name = bin_name + "_sig",
+        base = ":" + bin_name,
+      )

+ 31 - 0
infra/control/uuid.go

@@ -0,0 +1,31 @@
+package control
+
+import (
+	"fmt"
+
+	"v2ray.com/core/common"
+	"v2ray.com/core/common/uuid"
+)
+
+type UUIDCommand struct{}
+
+func (c *UUIDCommand) Name() string {
+	return "uuid"
+}
+
+func (c *UUIDCommand) Description() Description {
+	return Description{
+		Short: "Generate new UUIDs",
+		Usage: []string{"v2ctl uuid"},
+	}
+}
+
+func (c *UUIDCommand) Execute([]string) error {
+	u := uuid.New()
+	fmt.Println(u.String())
+	return nil
+}
+
+func init() {
+	common.Must(RegisterCommand(&UUIDCommand{}))
+}

+ 137 - 0
infra/control/verify.go

@@ -0,0 +1,137 @@
+package control
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"strings"
+
+	"golang.org/x/crypto/openpgp"
+
+	"v2ray.com/core/common"
+)
+
+const (
+	pubkey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
+Comment: GPGTools - https://gpgtools.org
+
+mQINBFiuFLcBEACtu5pycj7nHINq9gdkWtQhOdQPMRmbWPbCfxBRceIyB9IHUKay
+ldKEAA5DlOtub2ao811pLqcvcWMN61vmwDE9wcBBf1BRpoTb1XB4k60UDuCH4m9u
+r/XcwGaVBchiO8mdqCpB/h0rGXuoJ2Lqk4kXmyRZuaX2WUg7eOK9ZfslaaBc8lvI
+r5UvY7UL39LtzvOhQ+el2fXhktwZnCjDlovZzRVpn0QXXUAnuDuzCmd04NXjHZZB
+8q+h7jZrPrNusPzThkcaTUyuMqAHSrn0plNV1Ne0gDsUjGIOEoWtodnTeYGjkodu
+qipmLoFiFz0MsdD6CBs6LOr2OIjqJ8TtiMj2MqPiKZTVOb+hpmH1Cs6EN3IhCiLX
+QbiKX3UjBdVRIFlr4sL/JvOpLKr1RaEQS3nJ2m/Xuki1AOeKLoX8ebPca34tyXj0
+2gs7Khmfa02TI+fvcAlwzfwhDDab96SnKNOK6XDp0rh3ZTKVYeFhcN7m9z8FWHyJ
+O1onRVaq2bsKPX1Zv9ZC7jZIAMV2pC26UmRc7nJ/xdFj3tafA5hvILUifpO1qdlX
+iOCK+biPU3T9c6FakNiQ0sXAqhHbKaJNYcjDF3H3QIs1a35P7kfUJ+9Nc1WoCFGV
+Gh94dVLMGuoh+qo0A0qCg/y0/gGeZQ7G3jT5NXFx6UjlAb42R/dP+VSg6QARAQAB
+tCVPZmZpY2lhbCBSZWxlYXNlIDxvZmZpY2lhbEB2MnJheS5jb20+iQJUBBMBCgA+
+AhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAFiEEiwxeMlNgMveaPc7Z4a+lUMfT
+xJoFAlqRYBMFCQPF0FwACgkQ4a+lUMfTxJoymBAAnyqLfEdmP0ulki3uCQvIH4JD
+OXvFRyTLYweLehGqZ63i7yy0c1BzOsQbmQy2Trl2uiCgjOLmA6LdFB2d3rhsFssK
+fhFGroqCOHPdG7thSnBu9C0ohWdoiE1hfXVUtRn0P2vfqswNMdxwNwlZiRhWJemw
+1WmlaSXRp3PznC1eCYwUaS5IT18rzJyuk8z/Scb9DEWQwPhypz+NTE3j7qvQFmdP
+0cEDGUUXVe3HQ7oHlC+hzL79KttJeEMl575YbuLtAeRSJC0M+IgXd8YKuoORhqFM
+OwW4CNVMnAiF6mmb2Wf1hM+A9ydWVd3rz7sp3k1n4i5Hl4ftEz2cdicTX1JBG4ZB
+wsa9pfC5jk+negIQVvHPQRtWc/2bNYxNBF2cIpKF9wQ00E/wP64vl5QwJzs58Fqc
+cl3AwfskfvzeLSpdKlOCLE8FSQiKQ/NNw9fAuAe7YxW9xSKRTFGx8yQCNd11fmFe
+iMCDsBE9I51yUy8ywEtnedHi6mxMrnLv24VkD7jQZBWlvMDUEhGy2f6KgrSHTdEJ
+ZchSxfEIaM9Thy1E/3f6dQVkiPsf+/4wikS6sCdyW+ITVYc6yE5MvRz5oDjQH4z5
+JoELeuNxR59kpBErgdr8DBHSJNuxIT63QynrglwsG319Dzu5uPUC6WfqUGG9ATJ0
+neWkINHrf53bVk2rUG65Ag0EWK4UtwEQAL+11tnPaWlnbVj64j1Qikd+2gZRR7XF
+fNx1RHHHr4fyxmXUteZFS/L7QHJMDUYmVe6yiq6cvafuygqaUdrp8FLqapGZrsnj
+jH4i+h1cnZBiO4ui3mA/oaQM/FVjQDQ1LBeLlVxGDYhj/mlmDfYOIsd0wys0AmmW
+ytPsx0xXnbd9lkJpItfilAR+p7rbHc+755ZIIXPCOH1bXfJz+x0yafi7TgQgEC/M
+a4SeXVSpygKamZxYbdTpV355Fa4FHCAcK8v3+LnhE6c/4HXnGiuCAO3Lm1ZhgT3E
+xr8TjlWqdUFJiMmCAf9x8UidBoa6UGyW/yI55CbH35f5p3Tgq0k4Sjq8OrwC6qJm
+WGWv0HTCs9m21ie3yDKZljVfZ+gXSkaY84JbcYbmAEXH42Y/fEQdkhxxVELHt6Tk
+1bYvpW1NgRopw9U/mV8mERc0H6Vp+KoWU4uXiHK532YR4kUmvWh5WiSPFu/e6t5+
+/iWVwXVzvrDWx76cKuye1PgF/CmhKLc1JacJgaEtxuXvVXI4er+aTL/HbiISdzfc
+tYYdEVSYlkjJdV3/30HsupdsV/Y7O2DiGhlsGa5pKXVLmAvvHzdDfc2iKIbRSRWR
+kHni7uw/r/ZY78j5yBxwjZkopo3A5NJhByBOnNh9ZaWHBrc1a3WSsItGAn5ORHWk
+Q1KJY7SDFcXvABEBAAGJAiUEGAEKAA8FAliuFLcCGwwFCQHhM4AACgkQ4a+lUMfT
+xJrRCA//clpNxJahlPqEsdzCyYEGeXvI1dcZvUmEg+Nm6n1ohRVw1lqP+JbS31N4
+lByA93R2S5QVjMdr9KranXLC4F+bCJak5wbk7Pza9jqejf5f9nSqwc+M3EkMI2Sy
+2UlokDwK8m3yMtCf3vRDifvMGXpdUVsWreYvhY5owZfgYD1Ojy6toYqE31HGJEBM
+z+nGGKkAHVKOZbQAY9X6yAxGYuoV1Z2vddu7OJ4IMdqC4mxbndmKhsfGvotNVgFT
+WRW9DsKP+Im4WrNpcF7hxZFKNMlw3RbvrrFkCVYuejLUY9xEb57gqLT2APo0LmtX
+XfvJVB3X2uOelu/MAnnANmPg4Ej8J7D/Q+XX33IGXCrVXo0CVEPscFSqn6O94Ni8
+INpICE6G1EW/y+iZWcmjx59AnKYeFa40xgr/7TYZmouGBXfBNhtsghFlZY7Hw7ZD
+Ton1Wxcv14DPigiItYk7WkOiyPTLpAloWRSzs7GDFi2MQaFnrrrJ3ep0wHKuaaYl
+KJh08QdpalNSjGiga6boN1MH5FkI2NYAyGwQGvvcMe+TDEK43KcH4AssiZNtuXzx
+fkXkose778mzGzk5rBr0jGtKAxV2159CaI2KzR+uN7JwzoHrRRhVu/OWcaL/5MKq
+OUUihc22Z9/8GnKH1gscBhoIF+cqqOfzTIA6KrJHIC2u5Vpjvac=
+=xv/V
+-----END PGP PUBLIC KEY BLOCK-----
+`
+)
+
+func firstIdentity(m map[string]*openpgp.Identity) string {
+	for k := range m {
+		return k
+	}
+	return ""
+}
+
+type VerifyCommand struct{}
+
+func (c *VerifyCommand) Name() string {
+	return "verify"
+}
+
+func (c *VerifyCommand) Description() Description {
+	return Description{
+		Short: "Verify if a binary is officially signed.",
+		Usage: []string{
+			"v2ctl verify [--sig=<sig-file>] file",
+			"Verify the file officially signed by V2Ray.",
+		},
+	}
+}
+
+func (c *VerifyCommand) Execute(args []string) error {
+	fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+
+	sigFile := fs.String("sig", "", "Path to the signature file")
+
+	if err := fs.Parse(args); err != nil {
+		return err
+	}
+
+	target := fs.Arg(0)
+	if len(target) == 0 {
+		return newError("empty file path.")
+	}
+
+	if len(*sigFile) == 0 {
+		*sigFile = target + ".sig"
+	}
+
+	targetReader, err := os.Open(os.ExpandEnv(target))
+	if err != nil {
+		return newError("failed to open file: ", target).Base(err)
+	}
+
+	sigReader, err := os.Open(os.ExpandEnv(*sigFile))
+	if err != nil {
+		return newError("failed to open file ", *sigFile).Base(err)
+	}
+
+	keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(pubkey))
+	if err != nil {
+		return newError("failed to create keyring").Base(err)
+	}
+
+	entity, err := openpgp.CheckDetachedSignature(keyring, targetReader, sigReader)
+	if err != nil {
+		return newError("failed to verify signature").Base(err)
+	}
+
+	fmt.Println("Signed by:", firstIdentity(entity.Identities))
+	return nil
+}
+
+func init() {
+	common.Must(RegisterCommand(&VerifyCommand{}))
+}

+ 95 - 0
infra/vprotogen/main.go

@@ -0,0 +1,95 @@
+package main
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"v2ray.com/core/common"
+)
+
+var protocMap = map[string]string{
+	"windows": filepath.Join(os.Getenv("GOPATH"), "src", "v2ray.com", "core", ".dev", "protoc", "windows", "protoc.exe"),
+	"darwin":  filepath.Join(os.Getenv("GOPATH"), "src", "v2ray.com", "core", ".dev", "protoc", "macos", "protoc"),
+	"linux":   filepath.Join(os.Getenv("GOPATH"), "src", "v2ray.com", "core", ".dev", "protoc", "linux", "protoc"),
+}
+
+var (
+	repo = flag.String("repo", "", "Repo for protobuf generation, such as v2ray.com/core")
+)
+
+func main() {
+	flag.Parse()
+
+	protofiles := make(map[string][]string)
+	protoc := protocMap[runtime.GOOS]
+	gosrc := filepath.Join(os.Getenv("GOPATH"), "src")
+	reporoot := filepath.Join(os.Getenv("GOPATH"), "src", *repo)
+
+	filepath.Walk(reporoot, 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") {
+			protofiles[dir] = append(protofiles[dir], path)
+		}
+
+		return nil
+	})
+
+	for _, files := range protofiles {
+		args := []string{"--proto_path", gosrc, "--go_out", "plugins=grpc:" + gosrc}
+		args = append(args, files...)
+		cmd := exec.Command(protoc, args...)
+		cmd.Env = append(cmd.Env, os.Environ()...)
+		output, err := cmd.CombinedOutput()
+		if len(output) > 0 {
+			fmt.Println(string(output))
+		}
+		if err != nil {
+			fmt.Println(err)
+		}
+	}
+
+	common.Must(filepath.Walk(reporoot, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			fmt.Println(err)
+			return err
+		}
+
+		if info.IsDir() {
+			return nil
+		}
+
+		if !strings.HasSuffix(info.Name(), ".pb.go") {
+			return nil
+		}
+
+		content, err := ioutil.ReadFile(path)
+		if err != nil {
+			return err
+		}
+		content = bytes.Replace(content, []byte("\"golang.org/x/net/context\""), []byte("\"context\""), 1)
+
+		pos := bytes.Index(content, []byte("\npackage"))
+		if pos > 0 {
+			content = content[pos+1:]
+		}
+
+		return ioutil.WriteFile(path, content, info.Mode())
+	}))
+}

+ 1 - 1
main/jsonem/jsonem.go

@@ -3,7 +3,7 @@ package jsonem
 import (
 	"v2ray.com/core"
 	"v2ray.com/core/common"
-	"v2ray.com/ext/tools/conf/serial"
+	"v2ray.com/core/infra/conf/serial"
 )
 
 func init() {

+ 1 - 1
proto.go

@@ -2,5 +2,5 @@ package core
 
 //go:generate go get -u "github.com/golang/protobuf/protoc-gen-go"
 //go:generate go get -u "github.com/golang/protobuf/proto"
-//go:generate go install "v2ray.com/ext/tools/vprotogen"
+//go:generate go install "v2ray.com/core/infra/vprotogen"
 //go:generate vprotogen -repo v2ray.com/core