From 4eb2b5e607eba50c08aed62ca15b3c6f06583f3a Mon Sep 17 00:00:00 2001 From: Darien Raymond Date: Sun, 10 Feb 2019 19:04:11 +0100 Subject: [PATCH] merge ext into core --- app/router/condition_geoip_test.go | 20 +- app/router/condition_test.go | 16 +- common/platform/filesystem/file.go | 44 +++ infra/bazel/BUILD | 5 + infra/bazel/build.bzl | 66 ++++ infra/bazel/gpg.bzl | 23 ++ infra/bazel/matrix.bzl | 21 ++ infra/bazel/zip.bzl | 164 ++++++++ infra/conf/api.go | 39 ++ infra/conf/blackhole.go | 53 +++ infra/conf/blackhole_test.go | 34 ++ infra/conf/buildable.go | 7 + infra/conf/command/command.go | 48 +++ infra/conf/command/errors.generated.go | 9 + infra/conf/common.go | 189 ++++++++++ infra/conf/common_test.go | 211 +++++++++++ infra/conf/conf.go | 3 + infra/conf/dns.go | 176 +++++++++ infra/conf/dns_proxy.go | 12 + infra/conf/dns_test.go | 103 +++++ infra/conf/dokodemo.go | 28 ++ infra/conf/dokodemo_test.go | 41 ++ infra/conf/errors.generated.go | 9 + infra/conf/freedom.go | 57 +++ infra/conf/freedom_test.go | 43 +++ infra/conf/general_test.go | 36 ++ infra/conf/http.go | 35 ++ infra/conf/http_test.go | 39 ++ infra/conf/json/reader.go | 133 +++++++ infra/conf/json/reader_test.go | 97 +++++ infra/conf/loader.go | 83 ++++ infra/conf/log.go | 57 +++ infra/conf/mtproto.go | 69 ++++ infra/conf/mtproto_test.go | 40 ++ infra/conf/policy.go | 96 +++++ infra/conf/policy_test.go | 40 ++ infra/conf/reverse.go | 56 +++ infra/conf/reverse_test.go | 45 +++ infra/conf/router.go | 502 +++++++++++++++++++++++++ infra/conf/router_test.go | 233 ++++++++++++ infra/conf/serial/errors.generated.go | 9 + infra/conf/serial/loader.go | 71 ++++ infra/conf/serial/loader_test.go | 63 ++++ infra/conf/serial/serial.go | 3 + infra/conf/shadowsocks.go | 139 +++++++ infra/conf/shadowsocks_test.go | 36 ++ infra/conf/socks.go | 99 +++++ infra/conf/socks_test.go | 92 +++++ infra/conf/transport.go | 89 +++++ infra/conf/transport_authenticators.go | 223 +++++++++++ infra/conf/transport_internet.go | 476 +++++++++++++++++++++++ infra/conf/transport_test.go | 169 +++++++++ infra/conf/v2ray.go | 446 ++++++++++++++++++++++ infra/conf/v2ray_test.go | 338 +++++++++++++++++ infra/conf/vmess.go | 164 ++++++++ infra/conf/vmess_test.go | 117 ++++++ infra/control/api.go | 144 +++++++ infra/control/cert.go | 139 +++++++ infra/control/command.go | 51 +++ infra/control/control.go | 3 + infra/control/errors.generated.go | 9 + infra/control/fetch.go | 70 ++++ infra/control/love.go | 53 +++ infra/control/main/BUILD | 8 + infra/control/main/main.go | 48 +++ infra/control/main/targets.bzl | 56 +++ infra/control/uuid.go | 31 ++ infra/control/verify.go | 137 +++++++ infra/vprotogen/main.go | 95 +++++ main/jsonem/jsonem.go | 2 +- proto.go | 2 +- 71 files changed, 6346 insertions(+), 18 deletions(-) create mode 100644 common/platform/filesystem/file.go create mode 100644 infra/bazel/BUILD create mode 100644 infra/bazel/build.bzl create mode 100644 infra/bazel/gpg.bzl create mode 100644 infra/bazel/matrix.bzl create mode 100644 infra/bazel/zip.bzl create mode 100644 infra/conf/api.go create mode 100644 infra/conf/blackhole.go create mode 100644 infra/conf/blackhole_test.go create mode 100644 infra/conf/buildable.go create mode 100644 infra/conf/command/command.go create mode 100644 infra/conf/command/errors.generated.go create mode 100644 infra/conf/common.go create mode 100644 infra/conf/common_test.go create mode 100644 infra/conf/conf.go create mode 100644 infra/conf/dns.go create mode 100644 infra/conf/dns_proxy.go create mode 100644 infra/conf/dns_test.go create mode 100644 infra/conf/dokodemo.go create mode 100644 infra/conf/dokodemo_test.go create mode 100644 infra/conf/errors.generated.go create mode 100644 infra/conf/freedom.go create mode 100644 infra/conf/freedom_test.go create mode 100644 infra/conf/general_test.go create mode 100644 infra/conf/http.go create mode 100644 infra/conf/http_test.go create mode 100644 infra/conf/json/reader.go create mode 100644 infra/conf/json/reader_test.go create mode 100644 infra/conf/loader.go create mode 100644 infra/conf/log.go create mode 100644 infra/conf/mtproto.go create mode 100644 infra/conf/mtproto_test.go create mode 100644 infra/conf/policy.go create mode 100644 infra/conf/policy_test.go create mode 100644 infra/conf/reverse.go create mode 100644 infra/conf/reverse_test.go create mode 100644 infra/conf/router.go create mode 100644 infra/conf/router_test.go create mode 100644 infra/conf/serial/errors.generated.go create mode 100644 infra/conf/serial/loader.go create mode 100644 infra/conf/serial/loader_test.go create mode 100644 infra/conf/serial/serial.go create mode 100644 infra/conf/shadowsocks.go create mode 100644 infra/conf/shadowsocks_test.go create mode 100644 infra/conf/socks.go create mode 100644 infra/conf/socks_test.go create mode 100644 infra/conf/transport.go create mode 100644 infra/conf/transport_authenticators.go create mode 100644 infra/conf/transport_internet.go create mode 100644 infra/conf/transport_test.go create mode 100644 infra/conf/v2ray.go create mode 100644 infra/conf/v2ray_test.go create mode 100644 infra/conf/vmess.go create mode 100644 infra/conf/vmess_test.go create mode 100644 infra/control/api.go create mode 100644 infra/control/cert.go create mode 100644 infra/control/command.go create mode 100644 infra/control/control.go create mode 100644 infra/control/errors.generated.go create mode 100644 infra/control/fetch.go create mode 100644 infra/control/love.go create mode 100644 infra/control/main/BUILD create mode 100644 infra/control/main/main.go create mode 100644 infra/control/main/targets.bzl create mode 100644 infra/control/uuid.go create mode 100644 infra/control/verify.go create mode 100644 infra/vprotogen/main.go diff --git a/app/router/condition_geoip_test.go b/app/router/condition_geoip_test.go index b2fed7a5a..38e053736 100644 --- a/app/router/condition_geoip_test.go +++ b/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) diff --git a/app/router/condition_test.go b/app/router/condition_test.go index c12ecc9cb..428ca184e 100644 --- a/app/router/condition_test.go +++ b/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 { diff --git a/common/platform/filesystem/file.go b/common/platform/filesystem/file.go new file mode 100644 index 000000000..b41f69492 --- /dev/null +++ b/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 +} diff --git a/infra/bazel/BUILD b/infra/bazel/BUILD new file mode 100644 index 000000000..7902ba21b --- /dev/null +++ b/infra/bazel/BUILD @@ -0,0 +1,5 @@ +filegroup( + name = "rules", + srcs = glob(["*.bzl"]), + visibility = ["//visibility:public"], +) diff --git a/infra/bazel/build.bzl b/infra/bazel/build.bzl new file mode 100644 index 000000000..002f35cbc --- /dev/null +++ b/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, +) diff --git a/infra/bazel/gpg.bzl b/infra/bazel/gpg.bzl new file mode 100644 index 000000000..c0a879283 --- /dev/null +++ b/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"), + }, +) diff --git a/infra/bazel/matrix.bzl b/infra/bazel/matrix.bzl new file mode 100644 index 000000000..2b821d02f --- /dev/null +++ b/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"), +] diff --git a/infra/bazel/zip.bzl b/infra/bazel/zip.bzl new file mode 100644 index 000000000..ac6b12a34 --- /dev/null +++ b/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), + }, +) diff --git a/infra/conf/api.go b/infra/conf/api.go new file mode 100644 index 000000000..7856138d1 --- /dev/null +++ b/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 +} diff --git a/infra/conf/blackhole.go b/infra/conf/blackhole.go new file mode 100644 index 000000000..bd0ebf02c --- /dev/null +++ b/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", + "") +) diff --git a/infra/conf/blackhole_test.go b/infra/conf/blackhole_test.go new file mode 100644 index 000000000..cac4dd325 --- /dev/null +++ b/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{}, + }, + }) +} diff --git a/infra/conf/buildable.go b/infra/conf/buildable.go new file mode 100644 index 000000000..1d01cd66a --- /dev/null +++ b/infra/conf/buildable.go @@ -0,0 +1,7 @@ +package conf + +import "github.com/golang/protobuf/proto" + +type Buildable interface { + Build() (proto.Message, error) +} diff --git a/infra/conf/command/command.go b/infra/conf/command/command.go new file mode 100644 index 000000000..5c5e308cf --- /dev/null +++ b/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{})) +} diff --git a/infra/conf/command/errors.generated.go b/infra/conf/command/errors.generated.go new file mode 100644 index 000000000..66f780510 --- /dev/null +++ b/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{}) +} diff --git a/infra/conf/common.go b/infra/conf/common.go new file mode 100644 index 000000000..958b53367 --- /dev/null +++ b/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), + } +} diff --git a/infra/conf/common_test.go b/infra/conf/common_test.go new file mode 100644 index 000000000..1a4193e88 --- /dev/null +++ b/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") + } +} diff --git a/infra/conf/conf.go b/infra/conf/conf.go new file mode 100644 index 000000000..63b7313fe --- /dev/null +++ b/infra/conf/conf.go @@ -0,0 +1,3 @@ +package conf + +//go:generate errorgen diff --git a/infra/conf/dns.go b/infra/conf/dns.go new file mode 100644 index 000000000..9775b00aa --- /dev/null +++ b/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 +} diff --git a/infra/conf/dns_proxy.go b/infra/conf/dns_proxy.go new file mode 100644 index 000000000..8ac7bb226 --- /dev/null +++ b/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 +} diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go new file mode 100644 index 000000000..e99fbae15 --- /dev/null +++ b/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}, + }, + }, + }) +} diff --git a/infra/conf/dokodemo.go b/infra/conf/dokodemo.go new file mode 100644 index 000000000..c3b09b45f --- /dev/null +++ b/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 +} diff --git a/infra/conf/dokodemo_test.go b/infra/conf/dokodemo_test.go new file mode 100644 index 000000000..bc78fd618 --- /dev/null +++ b/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, + }, + }, + }) +} diff --git a/infra/conf/errors.generated.go b/infra/conf/errors.generated.go new file mode 100644 index 000000000..96798e2de --- /dev/null +++ b/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{}) +} diff --git a/infra/conf/freedom.go b/infra/conf/freedom.go new file mode 100644 index 000000000..44c36646d --- /dev/null +++ b/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 +} diff --git a/infra/conf/freedom_test.go b/infra/conf/freedom_test.go new file mode 100644 index 000000000..9f4f66425 --- /dev/null +++ b/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, + }, + }, + }) +} diff --git a/infra/conf/general_test.go b/infra/conf/general_test.go new file mode 100644 index 000000000..846b80234 --- /dev/null +++ b/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) + } + } +} diff --git a/infra/conf/http.go b/infra/conf/http.go new file mode 100644 index 000000000..5b7876330 --- /dev/null +++ b/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 +} diff --git a/infra/conf/http_test.go b/infra/conf/http_test.go new file mode 100644 index 000000000..87dd4f097 --- /dev/null +++ b/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, + }, + }, + }) +} diff --git a/infra/conf/json/reader.go b/infra/conf/json/reader.go new file mode 100644 index 000000000..6a654e51a --- /dev/null +++ b/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 +} diff --git a/infra/conf/json/reader_test.go b/infra/conf/json/reader_test.go new file mode 100644 index 000000000..900abcb20 --- /dev/null +++ b/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) + } + } + +} diff --git a/infra/conf/loader.go b/infra/conf/loader.go new file mode 100644 index 000000000..51f268a3e --- /dev/null +++ b/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 +} diff --git a/infra/conf/log.go b/infra/conf/log.go new file mode 100644 index 000000000..329849fa6 --- /dev/null +++ b/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 +} diff --git a/infra/conf/mtproto.go b/infra/conf/mtproto.go new file mode 100644 index 000000000..684e06a59 --- /dev/null +++ b/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 +} diff --git a/infra/conf/mtproto_test.go b/infra/conf/mtproto_test.go new file mode 100644 index 000000000..7c7d49111 --- /dev/null +++ b/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}, + }), + }, + }, + }, + }, + }) +} diff --git a/infra/conf/policy.go b/infra/conf/policy.go new file mode 100644 index 000000000..46e2a2cef --- /dev/null +++ b/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 +} diff --git a/infra/conf/policy_test.go b/infra/conf/policy_test.go new file mode 100644 index 000000000..ce6d4daf2 --- /dev/null +++ b/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) + } + } +} diff --git a/infra/conf/reverse.go b/infra/conf/reverse.go new file mode 100644 index 000000000..d9144255b --- /dev/null +++ b/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 +} diff --git a/infra/conf/reverse_test.go b/infra/conf/reverse_test.go new file mode 100644 index 000000000..c6c9e1da4 --- /dev/null +++ b/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"}, + }, + }, + }, + }) +} diff --git a/infra/conf/router.go b/infra/conf/router.go new file mode 100644 index 000000000..38eda6bf9 --- /dev/null +++ b/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 +} diff --git a/infra/conf/router_test.go b/infra/conf/router_test.go new file mode 100644 index 000000000..b6d0aa94a --- /dev/null +++ b/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", + }, + }, + }, + }, + }, + }) +} diff --git a/infra/conf/serial/errors.generated.go b/infra/conf/serial/errors.generated.go new file mode 100644 index 000000000..aeb5e222b --- /dev/null +++ b/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{}) +} diff --git a/infra/conf/serial/loader.go b/infra/conf/serial/loader.go new file mode 100644 index 000000000..1d084778d --- /dev/null +++ b/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 +} diff --git a/infra/conf/serial/loader_test.go b/infra/conf/serial/loader_test.go new file mode 100644 index 000000000..2bdda7bde --- /dev/null +++ b/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) + } + } +} diff --git a/infra/conf/serial/serial.go b/infra/conf/serial/serial.go new file mode 100644 index 000000000..8314cb013 --- /dev/null +++ b/infra/conf/serial/serial.go @@ -0,0 +1,3 @@ +package serial + +//go:generate errorgen diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go new file mode 100644 index 000000000..7e4e6194b --- /dev/null +++ b/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 +} diff --git a/infra/conf/shadowsocks_test.go b/infra/conf/shadowsocks_test.go new file mode 100644 index 000000000..608ee70f9 --- /dev/null +++ b/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}, + }, + }, + }) +} diff --git a/infra/conf/socks.go b/infra/conf/socks.go new file mode 100644 index 000000000..cba5989a6 --- /dev/null +++ b/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 +} diff --git a/infra/conf/socks_test.go b/infra/conf/socks_test.go new file mode 100644 index 000000000..3889a171d --- /dev/null +++ b/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", + }), + }, + }, + }, + }, + }, + }, + }) +} diff --git a/infra/conf/transport.go b/infra/conf/transport.go new file mode 100644 index 000000000..d9d09bd35 --- /dev/null +++ b/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 +} diff --git a/infra/conf/transport_authenticators.go b/infra/conf/transport_authenticators.go new file mode 100644 index 000000000..29092d3a4 --- /dev/null +++ b/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 +} diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go new file mode 100644 index 000000000..45d8b2ac9 --- /dev/null +++ b/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 +} diff --git a/infra/conf/transport_test.go b/infra/conf/transport_test.go new file mode 100644 index 000000000..d547482f8 --- /dev/null +++ b/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{}), + }), + }, + }, + }, + }, + }) +} diff --git a/infra/conf/v2ray.go b/infra/conf/v2ray.go new file mode 100644 index 000000000..a937d780b --- /dev/null +++ b/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 +} diff --git a/infra/conf/v2ray_test.go b/infra/conf/v2ray_test.go new file mode 100644 index 000000000..39eb24ae3 --- /dev/null +++ b/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, + }, + }), + }, + }, + }), + }, + }, + }, + }, + }) +} diff --git a/infra/conf/vmess.go b/infra/conf/vmess.go new file mode 100644 index 000000000..d243305aa --- /dev/null +++ b/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 +} diff --git a/infra/conf/vmess_test.go b/infra/conf/vmess_test.go new file mode 100644 index 000000000..ad3a2e1fa --- /dev/null +++ b/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, + }, + }, + }) +} diff --git a/infra/control/api.go b/infra/control/api.go new file mode 100644 index 000000000..75c4a2457 --- /dev/null +++ b/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{})) +} diff --git a/infra/control/cert.go b/infra/control/cert.go new file mode 100644 index 000000000..bcda6974b --- /dev/null +++ b/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{})) +} diff --git a/infra/control/command.go b/infra/control/command.go new file mode 100644 index 000000000..3bd3c8040 --- /dev/null +++ b/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()) + } +} diff --git a/infra/control/control.go b/infra/control/control.go new file mode 100644 index 000000000..adac14d85 --- /dev/null +++ b/infra/control/control.go @@ -0,0 +1,3 @@ +package control + +//go:generate errorgen diff --git a/infra/control/errors.generated.go b/infra/control/errors.generated.go new file mode 100644 index 000000000..143c6a0df --- /dev/null +++ b/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{}) +} diff --git a/infra/control/fetch.go b/infra/control/fetch.go new file mode 100644 index 000000000..167c7483c --- /dev/null +++ b/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 "}, + } +} + +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{})) +} diff --git a/infra/control/love.go b/infra/control/love.go new file mode 100644 index 000000000..a911ed422 --- /dev/null +++ b/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{})) +} diff --git a/infra/control/main/BUILD b/infra/control/main/BUILD new file mode 100644 index 000000000..95a670332 --- /dev/null +++ b/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) diff --git a/infra/control/main/main.go b/infra/control/main/main.go new file mode 100644 index 000000000..5c41aeb0e --- /dev/null +++ b/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 ") + 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) + } + } +} diff --git a/infra/control/main/targets.bzl b/infra/control/main/targets.bzl new file mode 100644 index 000000000..c97e6f947 --- /dev/null +++ b/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, + ) diff --git a/infra/control/uuid.go b/infra/control/uuid.go new file mode 100644 index 000000000..d3ab47f38 --- /dev/null +++ b/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{})) +} diff --git a/infra/control/verify.go b/infra/control/verify.go new file mode 100644 index 000000000..e03419a26 --- /dev/null +++ b/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=] 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{})) +} diff --git a/infra/vprotogen/main.go b/infra/vprotogen/main.go new file mode 100644 index 000000000..403b61e91 --- /dev/null +++ b/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()) + })) +} diff --git a/main/jsonem/jsonem.go b/main/jsonem/jsonem.go index 0ce814dac..7b58bb9b5 100644 --- a/main/jsonem/jsonem.go +++ b/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() { diff --git a/proto.go b/proto.go index 769bcbcae..779cdf8dc 100644 --- a/proto.go +++ b/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