Merge pull request #5 from 9seconds/tgproxies

Refactoring
This commit is contained in:
Sergey Arkhipov
2018-06-19 11:34:26 +03:00
committed by GitHub
26 changed files with 678 additions and 452 deletions
+4 -3
View File
@@ -10,6 +10,7 @@ RUN set -x \
curl \ curl \
git \ git \
make \ make \
upx \
&& update-ca-certificates && update-ca-certificates
ADD . /go/src/github.com/9seconds/mtg ADD . /go/src/github.com/9seconds/mtg
@@ -17,7 +18,8 @@ ADD . /go/src/github.com/9seconds/mtg
RUN set -x \ RUN set -x \
&& cd /go/src/github.com/9seconds/mtg \ && cd /go/src/github.com/9seconds/mtg \
&& make clean \ && make clean \
&& make -j 4 static && make -j 4 static \
&& upx --ultra-brute -qq ./mtg
############################################################################### ###############################################################################
@@ -29,8 +31,7 @@ ENTRYPOINT ["/usr/local/bin/mtg"]
ENV MTG_IP=0.0.0.0 \ ENV MTG_IP=0.0.0.0 \
MTG_PORT=3128 \ MTG_PORT=3128 \
MTG_STATS_IP=0.0.0.0 \ MTG_STATS_IP=0.0.0.0 \
MTG_STATS_PORT=3129 \ MTG_STATS_PORT=3129
MTG_USE_IPV6=true
EXPOSE 3128 3129 EXPOSE 3128 3129
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
+2 -2
View File
@@ -1,7 +1,7 @@
ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
IMAGE_NAME := mtg IMAGE_NAME := mtg
APP_NAME := $(IMAGE_NAME) APP_NAME := $(IMAGE_NAME)
GOMETALINTER := gometalinter.v2 GOMETALINTER := gometalinter
VENDOR_FILES := $(shell find "$(ROOT_DIR)/vendor" 2>/dev/null || echo -n "vendor") VENDOR_FILES := $(shell find "$(ROOT_DIR)/vendor" 2>/dev/null || echo -n "vendor")
CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,windows,darwin,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}") CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,windows,darwin,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}")
@@ -76,5 +76,5 @@ install-dep:
.PHONY: install-lint .PHONY: install-lint
install-lint: install-lint:
@go get gopkg.in/alecthomas/gometalinter.v2 && \ @go get github.com/alecthomas/gometalinter && \
$(GOMETALINTER) --install >/dev/null $(GOMETALINTER) --install >/dev/null
+27 -99
View File
@@ -8,109 +8,35 @@ Bullshit-free MTPROTO proxy for Telegram
# Rationale # Rationale
Telegram supports proxies and proxies act as a shield for censorship There are several available proxies for Telegram MTPROTO available. Here
and blocking actions of different goverments. At the moment of writing, are the most notable:
Telegram supports 2 types of proxies:
1. SOCKS5 * [Official](https://github.com/TelegramMessenger/MTProxy)
2. MTPROTO * [Python](https://github.com/alexbers/mtprotoproxy)
* [Erlang](https://github.com/seriyps/mtproto_proxy)
* [JS](https://github.com/FreedomPrevails/JSMTProxy)
SOCKS5 proxy is general SOCKS proxy as defined in Almost all of them follow the way how official proxy was build. This
[RFC1928](https://www.ietf.org/rfc/rfc1928.txt). The problem is that includes support of multiple secrets, support of promoted channels etc.
by default SOCKS5 proxy has an access to the whole internet so a lot
of people tend to hide them "just for a case". It is possible to setup
SOCKS5 proxy so it is able to access just some IPs/CIDRs but, you know,
yeah.
MTPROTO proxy is a native Telegram proxy. It has several advantages: mtg is an implementation in golang which is intended to be:
1. Traffic is obfuscated by AES-CTR; * **Lightweight**
2. It allows connections only to Telegram services; It has to consume as less resources as possible but not by losing
3. It gives proxy maintainer an ability to promote its channel. maintainability.
* **Easily deployable**
But in reality, MTPROTO have 2 advantages (from my biased view): I strongly believe that Telegram proxies should follow the way of
ShadowSocks: promoted channels is a strange way of doing business
1. Obfuscation I suppose. I think the only viable way is to have a proxy with
2. Simplify connection chain. minimum configuration which should work everywhere.
* **Single secret**
Here is how it looks like to work with SOCKS5 proxy: I think that multiple secrets solves no problems and just complexify
software. I also believe that in case of throwout proxies, this feature
``` is useless luxury.
Client -> SOCKS -> MTPROTO -> Telegram * **Minimum docker image size**
``` Official image is less than 2 megabytes. Literally.
* **No management WebUI**
SOCKS5 connects to IPs of Telegram proxies. AFAIK this is because This is an implementation of simple lightweight proxy. I won't do that.
Telegram wants us to avoid censorship and regulations.
What MTPROTO proxies do:
```
Client -> MTPROTO -> Telegram
```
And promoted channels. I do not tend to use them because mtg was created
for slightly other way of using it but yeah. People want moneys.
There are a number of unofficial proxies and one
[OFFICIAL](https://github.com/TelegramMessenger/MTProxy), so why bother?
<start-biased-rant>
I'm a big fan of [ShadowSocks](http://www.shadowsocks.org/en/index.html)
project and I like how people use it. The majority of SS proxies are
disposable ones which are blocked/unblocked frequently. There are some
public lists of them in Internet so if one proxy has stopped to work,
you throw it out and use another one.
Some SS proxies are long-living. This is because they are not public and
intended to be used only by limited number of people. And single secret
is fine there.
What I do not get about official and some unofficial implementation is
why they decided to support multiple secrets? I mean, WTF with all of
you?
1. MTPROTO obfuscation (called obfuscated2) does not allow to verify
client easily. You need to decrypt the frame for every secret. So, you
need a number of workers which will constantly try to crack initial
handshake frames with a list of secrets. That does not scale and will
never be.
2. Why do you need a multiple secrets? Which task are you trying to
solve with them? Valid secret means only 1 thing: access to Telegram. A
binary thing. Absurd and rudimentarty access control.
Okay, you want to revoke an access, thats fine. Will you ssh to the
machine and restart the container? Do you want to have API for that? Web
UI? Maybe store secrets in database and collect statisitcs per each?
With all respect, this is idiotic thing. Guysngals, this is a proxy.
Gateway to Telegram. This is not a webservice, or SASS or name that
shit. This is disposable stuff. Blocked? Fine, go to the next one. Just
look at ShadowSocks. There is multiple user implementation available,
with control you want. Does anyone gives a flying fuck about it?
> Those Who Do Not Learn History Are Doomed To Repeat It
- George Santayana
What I want to have?
1. Minimal tool for me and my friends (which are not all my FB friends but
a limited number of close friends).
2. Minimum viable configuration.
3. Single artifact runnable on every platform (not always Docker, some
environments may have no Docker)
4. Smallest Docker image
5. Lightweight
6. Have as less management as possible.
</end-biased-rant>
So, please do not ask for:
1. Multiple users/secrets
2. Web UI
3. Detailed statistics/histograms etc.
# How to build # How to build
@@ -170,3 +96,5 @@ $ docker run --name mtg --restart=unless-stopped -p 444:3128 -p 3129:3129 -d nin
You will have this tool up and running on port 444. Now curl You will have this tool up and running on port 444. Now curl
`localhost:3129` to get `tg://` links or do `docker logs mtg`. Also, `localhost:3129` to get `tg://` links or do `docker logs mtg`. Also,
port 3129 will show you some statistics if you are interested in. port 3129 will show you some statistics if you are interested in.
Also, you can use [run-mtg.sh](https://github.com/9seconds/mtg/blob/master/run-mtg.sh) script
+11
View File
@@ -0,0 +1,11 @@
package client
import (
"io"
"net"
"github.com/9seconds/mtg/config"
)
// Init has to initialize client connection based on given config.
type Init func(net.Conn, *config.Config) (int16, io.ReadWriteCloser, error)
+30
View File
@@ -0,0 +1,30 @@
package client
import (
"io"
"net"
"github.com/juju/errors"
"github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/obfuscated2"
"github.com/9seconds/mtg/wrappers"
)
// DirectInit initializes client to access Telegram bypassing middleproxies.
func DirectInit(conn net.Conn, conf *config.Config) (int16, io.ReadWriteCloser, error) {
socket := wrappers.NewTimeoutRWC(conn, conf.TimeoutRead, conf.TimeoutWrite)
frame, err := obfuscated2.ExtractFrame(socket)
if err != nil {
return 0, nil, errors.Annotate(err, "Cannot extract frame")
}
obfs2, dc, err := obfuscated2.ParseObfuscated2ClientFrame(conf.Secret, frame)
if err != nil {
return 0, nil, errors.Annotate(err, "Cannot parse obfuscated frame")
}
socket = wrappers.NewStreamCipherRWC(socket, obfs2.Encryptor, obfs2.Decryptor)
return dc, socket, nil
}
+142
View File
@@ -0,0 +1,142 @@
package config
import (
"encoding/hex"
"fmt"
"net"
"strconv"
"time"
"github.com/juju/errors"
)
// Config represents common configuration of mtg.
type Config struct {
Debug bool
Verbose bool
BindPort uint16
PublicIPv4Port uint16
PublicIPv6Port uint16
StatsPort uint16
TimeoutRead time.Duration
TimeoutWrite time.Duration
BindIP net.IP
PublicIPv4 net.IP
PublicIPv6 net.IP
StatsIP net.IP
Secret []byte
}
// URLs contains links to the proxy (tg://, t.me) and their QR codes.
type URLs struct {
TG string `json:"tg_url"`
TMe string `json:"tme_url"`
TGQRCode string `json:"tg_qrcode"`
TMeQRCode string `json:"tme_qrcode"`
}
// IPURLs contains links to both ipv4 and ipv6 of the proxy.
type IPURLs struct {
IPv4 URLs `json:"ipv4"`
IPv6 URLs `json:"ipv6"`
}
// BindAddr returns connection for this server to bind to.
func (c *Config) BindAddr() string {
return getAddr(c.BindIP, c.BindPort)
}
// IPv4Addr returns connection string to ipv6 for mtproto proxy.
func (c *Config) IPv4Addr() string {
return getAddr(c.PublicIPv4, c.PublicIPv4Port)
}
// IPv6Addr returns connection string to ipv6 for mtproto proxy.
func (c *Config) IPv6Addr() string {
return getAddr(c.PublicIPv6, c.PublicIPv6Port)
}
// StatAddr returns connection string to the stats API.
func (c *Config) StatAddr() string {
return getAddr(c.StatsIP, c.StatsPort)
}
// GetURLs returns configured IPURLs instance with links to this server.
func (c *Config) GetURLs() IPURLs {
return IPURLs{
IPv4: getURLs(c.PublicIPv4, c.PublicIPv4Port, c.Secret),
IPv6: getURLs(c.PublicIPv6, c.PublicIPv6Port, c.Secret),
}
}
func getAddr(host fmt.Stringer, port uint16) string {
return net.JoinHostPort(host.String(), strconv.Itoa(int(port)))
}
// NewConfig returns new configuration. If required, it manages and
// fetches data from external sources. Parameters passed to this
// function, should come from command line arguments.
func NewConfig(debug, verbose bool, // nolint: gocyclo
bindIP net.IP, bindPort uint16,
publicIPv4 net.IP, PublicIPv4Port uint16,
publicIPv6 net.IP, publicIPv6Port uint16,
statsIP net.IP, statsPort uint16,
timeoutRead, timeoutWrite time.Duration,
secret string) (*Config, error) {
secretBytes, err := hex.DecodeString(secret)
if err != nil {
return nil, errors.Annotate(err, "Cannot create config")
}
if publicIPv4 == nil {
publicIPv4, err = getGlobalIPv4()
if err != nil {
return nil, errors.Errorf("Cannot get public IP")
}
}
if publicIPv4.To4() == nil {
return nil, errors.Errorf("IP %s is not IPv4", publicIPv4.String())
}
if PublicIPv4Port == 0 {
PublicIPv4Port = bindPort
}
if publicIPv6 == nil {
publicIPv6, err = getGlobalIPv6()
if err != nil {
publicIPv6 = publicIPv4
}
}
if publicIPv6.To16() == nil {
return nil, errors.Errorf("IP %s is not IPv6", publicIPv6.String())
}
if publicIPv6Port == 0 {
publicIPv6Port = bindPort
}
if statsIP == nil {
statsIP = publicIPv4
}
conf := &Config{
Debug: debug,
Verbose: verbose,
BindIP: bindIP,
BindPort: bindPort,
PublicIPv4: publicIPv4,
PublicIPv4Port: PublicIPv4Port,
PublicIPv6: publicIPv6,
PublicIPv6Port: publicIPv6Port,
StatsIP: statsIP,
StatsPort: statsPort,
TimeoutRead: timeoutRead,
TimeoutWrite: timeoutWrite,
Secret: secretBytes,
}
return conf, nil
}
+39
View File
@@ -0,0 +1,39 @@
package config
import (
"io/ioutil"
"net"
"net/http"
"strings"
"github.com/juju/errors"
)
func getGlobalIPv4() (net.IP, error) {
return fetchIP("https://v4.ifconfig.co/ip")
}
func getGlobalIPv6() (net.IP, error) {
return fetchIP("https://v6.ifconfig.co/ip")
}
func fetchIP(url string) (net.IP, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close() // nolint: errcheck
respDataBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
respData := strings.TrimSpace(string(respDataBytes))
ip := net.ParseIP(respData)
if ip == nil {
return nil, errors.Errorf("ifconfig.co returns incorrect IP %s", respData)
}
return ip, nil
}
+59
View File
@@ -0,0 +1,59 @@
package config
import (
"encoding/hex"
"net"
"net/url"
"strconv"
)
func getURLs(addr net.IP, port uint16, secret []byte) (urls URLs) {
values := url.Values{}
values.Set("server", addr.String())
values.Set("port", strconv.Itoa(int(port)))
values.Set("secret", hex.EncodeToString(secret))
urls.TG = makeTGURL(values)
urls.TMe = makeTMeURL(values)
urls.TGQRCode = makeQRCodeURL(urls.TG)
urls.TMeQRCode = makeQRCodeURL(urls.TG)
return
}
func makeTGURL(values url.Values) string {
tgURL := url.URL{
Scheme: "tg",
Host: "proxy",
RawQuery: values.Encode(),
}
return tgURL.String()
}
func makeTMeURL(values url.Values) string {
tMeURL := url.URL{
Scheme: "https",
Host: "t.me",
Path: "proxy",
RawQuery: values.Encode(),
}
return tMeURL.String()
}
func makeQRCodeURL(data string) string {
QRURL := url.URL{
Scheme: "https",
Host: "api.qrserver.com",
Path: "v1/create-qr-code",
}
values := url.Values{}
values.Set("qzone", "4")
values.Set("format", "svg")
values.Set("data", data)
QRURL.RawQuery = values.Encode()
return QRURL.String()
}
+38 -46
View File
@@ -3,18 +3,16 @@ package main
//go:generate scripts/generate_version.sh //go:generate scripts/generate_version.sh
import ( import (
"encoding/hex"
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil"
"net/http"
"os" "os"
"strings"
"github.com/9seconds/mtg/proxy"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
kingpin "gopkg.in/alecthomas/kingpin.v2" kingpin "gopkg.in/alecthomas/kingpin.v2"
"github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/proxy"
) )
var ( var (
@@ -28,8 +26,9 @@ var (
Short('v'). Short('v').
Envar("MTG_VERBOSE"). Envar("MTG_VERBOSE").
Bool() Bool()
bindIP = app.Flag("bind-ip", "Which IP to bind to."). bindIP = app.Flag("bind-ip", "Which IP to bind to.").
Short('i'). Short('b').
Envar("MTG_IP"). Envar("MTG_IP").
Default("127.0.0.1"). Default("127.0.0.1").
IP() IP()
@@ -38,11 +37,23 @@ var (
Envar("MTG_PORT"). Envar("MTG_PORT").
Default("3128"). Default("3128").
Uint16() Uint16()
portToShow = app.Flag("show-bind-port",
"Which port to show in URL. Default is the value of bind-port"). publicIPv4 = app.Flag("public-ipv4", "Which IPv4 address is public.").
Short('a'). Short('4').
Envar("MTG_SHOW_PORT"). Envar("MTG_IPV4").
IP()
publicIPv4Port = app.Flag("public-ipv4-port", "Which IPv4 port is public. Default is 'bind-port' value.").
Envar("MTG_IPV4_PORT").
Uint16() Uint16()
publicIPv6 = app.Flag("public-ipv6", "Which IPv6 address is public.").
Short('6').
Envar("MTG_IPV6").
IP()
publicIPv6Port = app.Flag("public-ipv6-port", "Which IPv6 port is public. Default is 'bind-port' value.").
Envar("MTG_IPV6_PORT").
Uint16()
statsIP = app.Flag("stats-ip", "Which IP bind stats server to"). statsIP = app.Flag("stats-ip", "Which IP bind stats server to").
Short('t'). Short('t').
Envar("MTG_STATS_IP"). Envar("MTG_STATS_IP").
@@ -53,6 +64,7 @@ var (
Envar("MTG_STATS_PORT"). Envar("MTG_STATS_PORT").
Default("3129"). Default("3129").
Uint16() Uint16()
readTimeout = app.Flag("read-timeout", "Socket read timeout."). readTimeout = app.Flag("read-timeout", "Socket read timeout.").
Short('r'). Short('r').
Envar("MTG_READ_TIMEOUT"). Envar("MTG_READ_TIMEOUT").
@@ -63,15 +75,6 @@ var (
Envar("MTG_WRITE_TIMEOUT"). Envar("MTG_WRITE_TIMEOUT").
Default("30s"). Default("30s").
Duration() Duration()
serverName = app.Flag("server-name",
"Which server name to use. Default is IP address resolved by ipify.").
Short('s').
Envar("MTG_SERVER").
String()
preferIPv6 = app.Flag("prefer-ipv6", "Use IPv6").
Short('6').
Envar("MTG_USE_IPV6").
Bool()
secret = app.Arg("secret", "Secret of this proxy.").Required().String() secret = app.Arg("secret", "Secret of this proxy.").Required().String()
) )
@@ -80,33 +83,22 @@ func main() {
app.Version(version) app.Version(version)
kingpin.MustParse(app.Parse(os.Args[1:])) kingpin.MustParse(app.Parse(os.Args[1:]))
secretBytes, err := hex.DecodeString(*secret) conf, err := config.NewConfig(*debug, *verbose,
*bindIP, *bindPort,
*publicIPv4, *publicIPv4Port,
*publicIPv6, *publicIPv6Port,
*statsIP, *statsPort,
*readTimeout, *writeTimeout,
*secret,
)
if err != nil { if err != nil {
usage("Secret has to be hexadecimal string.") usage(err.Error())
}
if *portToShow == 0 {
*portToShow = *bindPort
}
if *serverName == "" {
resp, err := http.Get("https://api.ipify.org")
if err != nil || resp.StatusCode != http.StatusOK {
usage("Cannot get local IP address.")
}
myIPBytes, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() // nolint: errcheck
if err != nil {
usage("Cannot get local IP address.")
}
*serverName = strings.TrimSpace(string(myIPBytes))
} }
atom := zap.NewAtomicLevel() atom := zap.NewAtomicLevel()
if *debug { if conf.Debug {
atom.SetLevel(zapcore.DebugLevel) atom.SetLevel(zapcore.DebugLevel)
} else if *verbose { } else if conf.Verbose {
atom.SetLevel(zapcore.InfoLevel) atom.SetLevel(zapcore.InfoLevel)
} else { } else {
atom.SetLevel(zapcore.ErrorLevel) atom.SetLevel(zapcore.ErrorLevel)
@@ -118,12 +110,12 @@ func main() {
atom, atom,
)).Sugar() )).Sugar()
stat := proxy.NewStats(*serverName, *portToShow, *secret) stat := proxy.NewStats(conf)
go stat.Serve(*statsIP, *statsPort) go stat.Serve()
printURLs(stat.URLs)
srv := proxy.NewServer(conf, logger, stat)
printURLs(conf.GetURLs())
srv := proxy.NewServer(*bindIP, int(*bindPort), secretBytes, logger,
*readTimeout, *writeTimeout, *preferIPv6, stat)
if err := srv.Serve(); err != nil { if err := srv.Serve(); err != nil {
logger.Fatal(err.Error()) logger.Fatal(err.Error())
} }
+1 -7
View File
@@ -58,13 +58,7 @@ func (f Frame) DC() (n int16) {
n = 1 n = 1
} }
if n < 0 { return
n = -n
} else if n == 0 {
n = 1
}
return n - 1
} }
// Valid checks that *decrypted* frame is valid. Only magic bytes are checked. // Valid checks that *decrypted* frame is valid. Only magic bytes are checked.
+1 -1
View File
@@ -34,7 +34,7 @@ func TestFrameMagic(t *testing.T) {
} }
func TestFrameDC(t *testing.T) { func TestFrameDC(t *testing.T) {
assert.Equal(t, int16(770), makeFrame().DC()) assert.Equal(t, int16(771), makeFrame().DC())
} }
func TestFrameValid(t *testing.T) { func TestFrameValid(t *testing.T) {
+7 -23
View File
@@ -11,31 +11,15 @@ import (
// Obfuscated2 contains AES CTR encryption and decryption streams // Obfuscated2 contains AES CTR encryption and decryption streams
// for telegram connection. // for telegram connection.
type Obfuscated2 struct { type Obfuscated2 struct {
decryptor cipher.Stream Decryptor cipher.Stream
encryptor cipher.Stream Encryptor cipher.Stream
}
// Encrypt encrypts given data.
func (o *Obfuscated2) Encrypt(data []byte) []byte {
buf := make([]byte, len(data))
o.encryptor.XORKeyStream(buf, data)
return buf
}
// Decrypt decrypts given data.
func (o *Obfuscated2) Decrypt(data []byte) []byte {
buf := make([]byte, len(data))
o.decryptor.XORKeyStream(buf, data)
return buf
} }
// ParseObfuscated2ClientFrame parses client frame. Please check this link for // ParseObfuscated2ClientFrame parses client frame. Please check this link for
// details: http://telegra.ph/telegram-blocks-wtf-05-26 // details: http://telegra.ph/telegram-blocks-wtf-05-26
// //
// Beware, link above is in russian. // Beware, link above is in russian.
func ParseObfuscated2ClientFrame(secret, data []byte) (*Obfuscated2, int16, error) { func ParseObfuscated2ClientFrame(secret []byte, frame Frame) (*Obfuscated2, int16, error) {
frame := Frame(data)
decHasher := sha256.New() decHasher := sha256.New()
decHasher.Write(frame.Key()) // nolint: errcheck decHasher.Write(frame.Key()) // nolint: errcheck
decHasher.Write(secret) // nolint: errcheck decHasher.Write(secret) // nolint: errcheck
@@ -54,8 +38,8 @@ func ParseObfuscated2ClientFrame(secret, data []byte) (*Obfuscated2, int16, erro
} }
obfs := &Obfuscated2{ obfs := &Obfuscated2{
decryptor: decryptor, Decryptor: decryptor,
encryptor: encryptor, Encryptor: encryptor,
} }
return obfs, decryptedFrame.DC(), nil return obfs, decryptedFrame.DC(), nil
@@ -77,8 +61,8 @@ func MakeTelegramObfuscated2Frame() (*Obfuscated2, Frame) {
copy(frame, copyFrame) copy(frame, copyFrame)
obfs := &Obfuscated2{ obfs := &Obfuscated2{
decryptor: decryptor, Decryptor: decryptor,
encryptor: encryptor, Encryptor: encryptor,
} }
return obfs, frame return obfs, frame
+6 -3
View File
@@ -25,7 +25,8 @@ func TestObfs2TelegramDecryptEncryptDecrypt(t *testing.T) {
data := []byte{1, 2, 3} data := []byte{1, 2, 3}
encrypted := make([]byte, 3) encrypted := make([]byte, 3)
encryptor.XORKeyStream(encrypted, data) encryptor.XORKeyStream(encrypted, data)
decrypted := obfs2.Decrypt(encrypted) decrypted := make([]byte, 3)
obfs2.Decryptor.XORKeyStream(decrypted, encrypted)
assert.Equal(t, data, decrypted) assert.Equal(t, data, decrypted)
} }
@@ -67,10 +68,12 @@ func TestObfs2Full(t *testing.T) {
tgEncryptedMessage := make([]byte, len(message)) tgEncryptedMessage := make([]byte, len(message))
tgEncryptor.XORKeyStream(tgEncryptedMessage, message) tgEncryptor.XORKeyStream(tgEncryptedMessage, message)
tgEncDecryptedMessage := tgObfs.Decrypt(tgEncryptedMessage) tgEncDecryptedMessage := make([]byte, len(tgEncryptedMessage))
tgObfs.Decryptor.XORKeyStream(tgEncDecryptedMessage, tgEncryptedMessage)
assert.Equal(t, message, tgEncDecryptedMessage) assert.Equal(t, message, tgEncDecryptedMessage)
clientEncryptedMessage := clientObfs.Encrypt(tgEncDecryptedMessage) clientEncryptedMessage := make([]byte, len(tgEncDecryptedMessage))
clientObfs.Encryptor.XORKeyStream(clientEncryptedMessage, tgEncDecryptedMessage)
finalMessage := make([]byte, len(clientEncryptedMessage)) finalMessage := make([]byte, len(clientEncryptedMessage))
clientDecryptor.XORKeyStream(finalMessage, clientEncryptedMessage) clientDecryptor.XORKeyStream(finalMessage, clientEncryptedMessage)
-56
View File
@@ -1,56 +0,0 @@
package proxy
import (
"bytes"
"io"
)
// Cipher is an interface to anything which can encrypt and decrypt
type Cipher interface {
Encrypt([]byte) []byte
Decrypt([]byte) []byte
}
// CipherReadWriteCloser wraps connection for transparent encryption
type CipherReadWriteCloser struct {
crypt Cipher
conn io.ReadWriteCloser
rest *bytes.Buffer
}
// Read reads from connection
func (c *CipherReadWriteCloser) Read(p []byte) (n int, err error) {
n, err = c.conn.Read(p)
copy(p, c.crypt.Decrypt(p[:n]))
return
}
// Write writes into connection.
func (c *CipherReadWriteCloser) Write(p []byte) (int, error) {
encrypted := c.crypt.Encrypt(p)
allWritten := 0
for len(encrypted) > 0 {
n, err := c.conn.Write(encrypted)
allWritten += n
if err != nil {
return allWritten, err
}
encrypted = encrypted[n:]
}
return allWritten, nil
}
// Close closes underlying connection.
func (c *CipherReadWriteCloser) Close() error {
return c.conn.Close()
}
func newCipherReadWriteCloser(conn io.ReadWriteCloser, crypt Cipher) *CipherReadWriteCloser {
return &CipherReadWriteCloser{
conn: conn,
crypt: crypt,
rest: &bytes.Buffer{},
}
}
+33 -57
View File
@@ -4,33 +4,30 @@ import (
"context" "context"
"io" "io"
"net" "net"
"strconv"
"sync" "sync"
"time"
"github.com/9seconds/mtg/obfuscated2"
"github.com/juju/errors" "github.com/juju/errors"
uuid "github.com/satori/go.uuid" uuid "github.com/satori/go.uuid"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/9seconds/mtg/client"
"github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/telegram"
"github.com/9seconds/mtg/wrappers"
) )
// Server is an insgtance of MTPROTO proxy. // Server is an insgtance of MTPROTO proxy.
type Server struct { type Server struct {
ip net.IP conf *config.Config
port int
secret []byte
logger *zap.SugaredLogger logger *zap.SugaredLogger
ctx context.Context
readTimeout time.Duration
writeTimeout time.Duration
stats *Stats stats *Stats
ipv6 bool tg telegram.Telegram
clientInit client.Init
} }
// Serve does MTPROTO proxying. // Serve does MTPROTO proxying.
func (s *Server) Serve() error { func (s *Server) Serve() error {
addr := net.JoinHostPort(s.ip.String(), strconv.Itoa(s.port)) lsock, err := net.Listen("tcp", s.conf.BindAddr())
lsock, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return errors.Annotate(err, "Cannot create listen socket") return errors.Annotate(err, "Cannot create listen socket")
} }
@@ -56,18 +53,16 @@ func (s *Server) accept(conn net.Conn) {
s.stats.newConnection() s.stats.newConnection()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
socketID := s.makeSocketID() socketID := uuid.NewV4().String()
s.logger.Debugw("Client connected", s.logger.Debugw("Client connected",
"secret", s.secret,
"addr", conn.RemoteAddr().String(), "addr", conn.RemoteAddr().String(),
"socketid", socketID, "socketid", socketID,
) )
clientConn, dc, err := s.getClientStream(ctx, cancel, conn, socketID) dc, clientConn, err := s.getClientStream(ctx, cancel, conn, socketID)
if err != nil { if err != nil {
s.logger.Warnw("Cannot initialize client connection", s.logger.Warnw("Cannot initialize client connection",
"secret", s.secret,
"addr", conn.RemoteAddr().String(), "addr", conn.RemoteAddr().String(),
"socketid", socketID, "socketid", socketID,
"error", err, "error", err,
@@ -100,68 +95,49 @@ func (s *Server) accept(conn net.Conn) {
wait.Wait() wait.Wait()
s.logger.Debugw("Client disconnected", s.logger.Debugw("Client disconnected",
"secret", s.secret,
"addr", conn.RemoteAddr().String(), "addr", conn.RemoteAddr().String(),
"socketid", socketID, "socketid", socketID,
) )
} }
func (s *Server) makeSocketID() string { func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, conn net.Conn, socketID string) (int16, io.ReadWriteCloser, error) {
return uuid.NewV4().String() dc, socket, err := s.clientInit(conn, s.conf)
}
func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, conn net.Conn, socketID string) (io.ReadWriteCloser, int16, error) {
wConn := newTimeoutReadWriteCloser(conn, s.readTimeout, s.writeTimeout)
wConn = newTrafficReadWriteCloser(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic)
frame, err := obfuscated2.ExtractFrame(wConn)
if err != nil { if err != nil {
return nil, 0, errors.Annotate(err, "Cannot create client stream") return 0, nil, errors.Annotate(err, "Cannot init client connection")
} }
obfs2, dc, err := obfuscated2.ParseObfuscated2ClientFrame(s.secret, frame) socket = wrappers.NewTrafficRWC(socket, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic)
if err != nil { socket = wrappers.NewLogRWC(socket, s.logger, socketID, "client")
return nil, 0, errors.Annotate(err, "Cannot create client stream") socket = wrappers.NewCtxRWC(ctx, cancel, socket)
}
wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "client") return dc, socket, nil
wConn = newCipherReadWriteCloser(wConn, obfs2)
wConn = newCtxReadWriteCloser(ctx, cancel, wConn)
return wConn, dc, nil
} }
func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFunc, dc int16, socketID string) (io.ReadWriteCloser, error) { func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFunc, dc int16, socketID string) (io.ReadWriteCloser, error) {
socket, err := dialToTelegram(s.ipv6, dc, s.readTimeout) conn, err := s.tg.Dial(dc)
if err != nil { if err != nil {
return nil, errors.Annotate(err, "Cannot dial") return nil, errors.Annotate(err, "Cannot connect to Telegram")
}
wConn := newTimeoutReadWriteCloser(socket, s.readTimeout, s.writeTimeout)
wConn = newTrafficReadWriteCloser(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic)
obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame()
if n, err := socket.Write(frame); err != nil || n != len(frame) {
return nil, errors.Annotate(err, "Cannot write hadnshake frame")
} }
wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "telegram") conn = wrappers.NewTrafficRWC(conn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic)
wConn = newCipherReadWriteCloser(wConn, obfs2) conn, err = s.tg.Init(conn)
wConn = newCtxReadWriteCloser(ctx, cancel, wConn) if err != nil {
return nil, errors.Annotate(err, "Cannot handshake Telegram")
}
return wConn, nil conn = wrappers.NewLogRWC(conn, s.logger, socketID, "telegram")
conn = wrappers.NewCtxRWC(ctx, cancel, conn)
return conn, nil
} }
// NewServer creates new instance of MTPROTO proxy. // NewServer creates new instance of MTPROTO proxy.
func NewServer(ip net.IP, port int, secret []byte, logger *zap.SugaredLogger, func NewServer(conf *config.Config, logger *zap.SugaredLogger, stat *Stats) *Server {
readTimeout, writeTimeout time.Duration, ipv6 bool, stat *Stats) *Server {
return &Server{ return &Server{
ip: ip, conf: conf,
port: port,
secret: secret,
ctx: context.Background(),
logger: logger, logger: logger,
readTimeout: readTimeout,
writeTimeout: writeTimeout,
stats: stat, stats: stat,
ipv6: ipv6, tg: telegram.NewDirectTelegram(conf),
clientInit: client.DirectInit,
} }
} }
+13 -66
View File
@@ -2,13 +2,12 @@ package proxy
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/9seconds/mtg/config"
) )
type statsUptime time.Time type statsUptime time.Time
@@ -26,13 +25,10 @@ type Stats struct {
Incoming uint64 `json:"incoming"` Incoming uint64 `json:"incoming"`
Outgoing uint64 `json:"outgoing"` Outgoing uint64 `json:"outgoing"`
} `json:"traffic"` } `json:"traffic"`
URLs struct { URLs config.IPURLs `json:"urls"`
TG string `json:"tg_url"`
TMe string `json:"tme_url"`
TGQRCode string `json:"tg_qrcode"`
TMeQRCode string `json:"tme_qrcode"`
} `json:"urls"`
Uptime statsUptime `json:"uptime"` Uptime statsUptime `json:"uptime"`
conf *config.Config
} }
func (s *Stats) newConnection() { func (s *Stats) newConnection() {
@@ -53,7 +49,7 @@ func (s *Stats) addOutgoingTraffic(n int) {
} }
// Serve runs statistics HTTP server. // Serve runs statistics HTTP server.
func (s *Stats) Serve(host fmt.Stringer, port uint16) { func (s *Stats) Serve() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -63,65 +59,16 @@ func (s *Stats) Serve(host fmt.Stringer, port uint16) {
encoder.Encode(s) // nolint: errcheck, gas encoder.Encode(s) // nolint: errcheck, gas
}) })
addr := net.JoinHostPort(host.String(), strconv.Itoa(int(port))) http.ListenAndServe(s.conf.StatAddr(), nil) // nolint: errcheck, gas
http.ListenAndServe(addr, nil) // nolint: errcheck, gas
} }
// NewStats returns new instance of statistics datastructure. // NewStats returns new instance of statistics datastructure.
func NewStats(serverName string, port uint16, secret string) *Stats { func NewStats(conf *config.Config) *Stats {
urlQuery := makeURLQuery(serverName, port, secret) stat := &Stats{
Uptime: statsUptime(time.Now()),
stat := &Stats{Uptime: statsUptime(time.Now())} conf: conf,
stat.URLs.TG = makeTGURL(urlQuery) }
stat.URLs.TMe = makeTMeURL(urlQuery) stat.URLs = conf.GetURLs()
stat.URLs.TGQRCode = makeQRCodeURL(stat.URLs.TG)
stat.URLs.TMeQRCode = makeQRCodeURL(stat.URLs.TMe)
return stat return stat
} }
func makeURLQuery(serverName string, port uint16, secret string) url.Values {
values := url.Values{}
values.Set("server", serverName)
values.Set("port", strconv.Itoa(int(port)))
values.Set("secret", secret)
return values
}
func makeTGURL(values url.Values) string {
tgURL := url.URL{
Scheme: "tg",
Host: "proxy",
RawQuery: values.Encode(),
}
return tgURL.String()
}
func makeTMeURL(values url.Values) string {
tMeURL := url.URL{
Scheme: "https",
Host: "t.me",
Path: "proxy",
RawQuery: values.Encode(),
}
return tMeURL.String()
}
func makeQRCodeURL(data string) string {
QRURL := url.URL{
Scheme: "https",
Host: "api.qrserver.com",
Path: "v1/create-qr-code",
}
values := url.Values{}
values.Set("qzone", "4")
values.Set("format", "svg")
values.Set("data", data)
QRURL.RawQuery = values.Encode()
return QRURL.String()
}
-75
View File
@@ -1,75 +0,0 @@
package proxy
import (
"net"
"time"
"github.com/juju/errors"
)
// TelegramAddress presents a pair of v4 and v6 addresses. This pairization
// is required because we want to use DC indexes.
type TelegramAddress struct {
v4 string
v6 string
}
// IPv4 returns v4 address.
func (t *TelegramAddress) IPv4() string {
return net.JoinHostPort(t.v4, telegramPort)
}
// IPv6 returns v4 address.
func (t *TelegramAddress) IPv6() string {
return net.JoinHostPort(t.v6, telegramPort)
}
// TelegramAddresses is a list of all known Telegram addresses for DC indexes.
var TelegramAddresses = []TelegramAddress{
TelegramAddress{v4: "149.154.175.50", v6: "2001:b28:f23d:f001::a"},
TelegramAddress{v4: "149.154.167.51", v6: "2001:67c:04e8:f002::a"},
TelegramAddress{v4: "149.154.175.100", v6: "2001:b28:f23d:f003::a"},
TelegramAddress{v4: "149.154.167.91", v6: "2001:67c:04e8:f004::a"},
TelegramAddress{v4: "149.154.171.5", v6: "2001:b28:f23f:f005::a"},
}
const telegramPort = "443"
const telegramKeepAlive = 30 * time.Second
func dialToTelegram(ipv6 bool, dcIdx int16, timeout time.Duration) (net.Conn, error) {
if dcIdx < 0 || dcIdx >= 5 {
return nil, errors.New("Incorrect DC IDX")
}
conn, err := doDial(ipv6, dcIdx, timeout)
if err != nil {
return nil, errors.Annotate(err, "Cannot dial")
}
if err := conn.SetKeepAlive(true); err != nil {
return nil, errors.Annotate(err, "Cannot establish keepalive connection")
}
if err := conn.SetKeepAlivePeriod(telegramKeepAlive); err != nil {
return nil, errors.Annotate(err, "Cannot set keepalive timeout")
}
return conn, nil
}
func doDial(ipv6 bool, dcIdx int16, timeout time.Duration) (*net.TCPConn, error) {
dialer := net.Dialer{Timeout: timeout}
addr := TelegramAddresses[dcIdx]
if ipv6 {
if conn, err := dialer.Dial("tcp", addr.IPv6()); err == nil {
return conn.(*net.TCPConn), nil
}
}
conn, err := dialer.Dial("tcp", addr.IPv4())
if err == nil {
return conn.(*net.TCPConn), nil
}
return nil, err
}
Executable
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
set -eu -o pipefail
IMAGE_NAME="nineseconds/mtg"
CONTAINER_NAME="mtg"
SECRET_PATH="$HOME/.mtg.secret"
PROXY_PORT=444
STAT_PORT=3129
[[ -e "$SECRET_PATH" ]] || (
openssl rand -hex 16 > "$SECRET_PATH"
chmod 0400 "$SECRET_PATH"
)
# docker pull "$IMAGE_NAME"
docker ps --filter "Name=$CONTAINER_NAME" -aq | xargs -r docker rm -fv
docker run \
-d \
--name "$CONTAINER_NAME" \
--sysctl 'net.ipv4.ip_local_port_range=10000 65000' \
--sysctl net.ipv4.tcp_congestion_control=bbr \
--sysctl net.ipv4.tcp_fastopen=3 \
--sysctl net.ipv4.tcp_fin_timeout=30 \
--sysctl net.ipv4.tcp_keepalive_time=1200 \
--sysctl net.ipv4.tcp_max_syn_backlog=4096 \
--sysctl net.ipv4.tcp_max_tw_buckets=5000 \
--sysctl net.ipv4.tcp_mtu_probing=1 \
--sysctl 'net.ipv4.tcp_rmem=4096 87380 67108864' \
--sysctl net.ipv4.tcp_syncookies=1 \
--sysctl net.ipv4.tcp_tw_reuse=1 \
--sysctl 'net.ipv4.tcp_wmem=4096 65536 67108864' \
--ulimit nofile=51200:51200 \
--restart=unless-stopped \
-p $PROXY_PORT:3128 \
-p $STAT_PORT:3129 \
"$IMAGE_NAME" "$(cat "$SECRET_PATH")"
+53
View File
@@ -0,0 +1,53 @@
package telegram
import (
"io"
"net"
"time"
"github.com/juju/errors"
"github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/wrappers"
)
const telegramKeepAlive = 30 * time.Second
type tgDialer struct {
net.Dialer
conf *config.Config
}
func (t *tgDialer) dial(addr string) (net.Conn, error) {
connRaw, err := t.Dialer.Dial("tcp", addr)
if err != nil {
return nil, errors.Annotate(err, "Cannot connect to Telegram")
}
conn := connRaw.(*net.TCPConn)
if err = conn.SetKeepAlive(true); err != nil {
return nil, errors.Annotate(err, "Cannot establish keepalive connection")
}
if err = conn.SetKeepAlivePeriod(telegramKeepAlive); err != nil {
return nil, errors.Annotate(err, "Cannot set keepalive timeout")
}
return conn, nil
}
func (t *tgDialer) dialRWC(addr string) (io.ReadWriteCloser, error) {
conn, err := t.dial(addr)
if err != nil {
return nil, err
}
return wrappers.NewTimeoutRWC(conn, t.conf.TimeoutRead, t.conf.TimeoutWrite), nil
}
func newDialer(conf *config.Config) *tgDialer {
return &tgDialer{
Dialer: net.Dialer{Timeout: conf.TimeoutRead},
conf: conf,
}
}
+61
View File
@@ -0,0 +1,61 @@
package telegram
import (
"io"
"github.com/juju/errors"
"github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/obfuscated2"
"github.com/9seconds/mtg/wrappers"
)
var (
directV4Addresses = map[int16][]string{
0: []string{"149.154.175.50:443"},
1: []string{"149.154.167.51:443"},
2: []string{"149.154.175.100:443"},
3: []string{"149.154.167.91:443"},
4: []string{"149.154.171.5:443"},
}
directV6Addresses = map[int16][]string{
0: []string{"[2001:b28:f23d:f001::a]:443"},
1: []string{"[2001:67c:04e8:f002::a]:443"},
2: []string{"[2001:b28:f23d:f003::a]:443"},
3: []string{"[2001:67c:04e8:f004::a]:443"},
4: []string{"[2001:b28:f23f:f005::a]:443"},
}
)
type directTelegram struct {
baseTelegram
}
func (t *directTelegram) Dial(dcIdx int16) (io.ReadWriteCloser, error) {
if dcIdx < 0 {
dcIdx = -dcIdx
} else if dcIdx == 0 {
dcIdx = 1
}
return t.baseTelegram.Dial(dcIdx - 1)
}
func (t *directTelegram) Init(conn io.ReadWriteCloser) (io.ReadWriteCloser, error) {
obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame()
if n, err := conn.Write(frame); err != nil || n != len(frame) {
return nil, errors.Annotate(err, "Cannot write hadnshake frame")
}
return wrappers.NewStreamCipherRWC(conn, obfs2.Encryptor, obfs2.Decryptor), nil
}
// NewDirectTelegram returns Telegram instance which connects directly
// to Telegram bypassing middleproxies.
func NewDirectTelegram(conf *config.Config) Telegram {
return &directTelegram{baseTelegram{
dialer: newDialer(conf),
v4Addresses: directV4Addresses,
v6Addresses: directV6Addresses,
}}
}
+41
View File
@@ -0,0 +1,41 @@
package telegram
import (
"io"
"math/rand"
"github.com/juju/errors"
)
// Telegram defines an interface to connect to Telegram. This
// encapsulates logic of working with middleproxies or direct
// connections.
type Telegram interface {
Dial(int16) (io.ReadWriteCloser, error)
Init(io.ReadWriteCloser) (io.ReadWriteCloser, error)
}
type baseTelegram struct {
dialer *tgDialer
v4Addresses map[int16][]string
v6Addresses map[int16][]string
}
func (b *baseTelegram) Dial(dcIdx int16) (io.ReadWriteCloser, error) {
addrs := make([]string, 2)
if addr, ok := b.v6Addresses[dcIdx]; ok && len(addr) > 0 {
addrs = append(addrs, addr[rand.Intn(len(addr))])
}
if addr, ok := b.v4Addresses[dcIdx]; ok && len(addr) > 0 {
addrs = append(addrs, addr[rand.Intn(len(addr))])
}
for _, addr := range addrs {
if conn, err := b.dialer.dialRWC(addr); err == nil {
return conn, err
}
}
return nil, errors.New("Cannot connect to Telegram")
}
+4 -2
View File
@@ -1,4 +1,4 @@
package proxy package wrappers
import ( import (
"context" "context"
@@ -48,7 +48,9 @@ func (c *CtxReadWriteCloser) Close() error {
return c.conn.Close() return c.conn.Close()
} }
func newCtxReadWriteCloser(ctx context.Context, cancel context.CancelFunc, conn io.ReadWriteCloser) io.ReadWriteCloser { // NewCtxRWC returns ReadWriteCloser which respects given context,
// cancellation etc.
func NewCtxRWC(ctx context.Context, cancel context.CancelFunc, conn io.ReadWriteCloser) io.ReadWriteCloser {
return &CtxReadWriteCloser{ return &CtxReadWriteCloser{
conn: conn, conn: conn,
ctx: ctx, ctx: ctx,
+3 -2
View File
@@ -1,4 +1,4 @@
package proxy package wrappers
import ( import (
"io" "io"
@@ -36,7 +36,8 @@ func (l *LogReadWriteCloser) Close() error {
return err return err
} }
func newLogReadWriteCloser(conn io.ReadWriteCloser, logger *zap.SugaredLogger, sockid string, name string) io.ReadWriteCloser { // NewLogRWC wraps ReadWriteCloser with logger calls.
func NewLogRWC(conn io.ReadWriteCloser, logger *zap.SugaredLogger, sockid string, name string) io.ReadWriteCloser {
return &LogReadWriteCloser{ return &LogReadWriteCloser{
conn: conn, conn: conn,
logger: logger, logger: logger,
+54
View File
@@ -0,0 +1,54 @@
package wrappers
import (
"crypto/cipher"
"io"
)
// StreamCipherReadWriteCloser is a ReadWriteCloser which ciphers
// incoming and outgoing data with givem cipher.Stream instances.
type StreamCipherReadWriteCloser struct {
encryptor cipher.Stream
decryptor cipher.Stream
conn io.ReadWriteCloser
}
// Read reads from connection
func (c *StreamCipherReadWriteCloser) Read(p []byte) (n int, err error) {
n, err = c.conn.Read(p)
c.decryptor.XORKeyStream(p, p[:n])
return
}
// Write writes into connection.
func (c *StreamCipherReadWriteCloser) Write(p []byte) (int, error) {
encrypted := make([]byte, len(p))
c.encryptor.XORKeyStream(encrypted, p)
allWritten := 0
for len(encrypted) > 0 {
n, err := c.conn.Write(encrypted)
allWritten += n
if err != nil {
return allWritten, err
}
encrypted = encrypted[n:]
}
return allWritten, nil
}
// Close closes underlying connection.
func (c *StreamCipherReadWriteCloser) Close() error {
return c.conn.Close()
}
// NewStreamCipherRWC returns wrapper which transparently
// encrypts/decrypts traffic with obfuscated2 protocol.
func NewStreamCipherRWC(conn io.ReadWriteCloser, encryptor, decryptor cipher.Stream) io.ReadWriteCloser {
return &StreamCipherReadWriteCloser{
conn: conn,
encryptor: encryptor,
decryptor: decryptor,
}
}
@@ -1,4 +1,4 @@
package proxy package wrappers
import ( import (
"io" "io"
@@ -31,7 +31,9 @@ func (t *TimeoutReadWriteCloser) Close() error {
return t.conn.Close() return t.conn.Close()
} }
func newTimeoutReadWriteCloser(conn net.Conn, readTimeout, writeTimeout time.Duration) io.ReadWriteCloser { // NewTimeoutRWC returns wrapper over net.Conn which sets deadlines for
// every wrapped Read/Write.
func NewTimeoutRWC(conn net.Conn, readTimeout, writeTimeout time.Duration) io.ReadWriteCloser {
return &TimeoutReadWriteCloser{ return &TimeoutReadWriteCloser{
conn: conn, conn: conn,
readTimeout: readTimeout, readTimeout: readTimeout,
@@ -1,4 +1,4 @@
package proxy package wrappers
import "io" import "io"
@@ -29,7 +29,8 @@ func (t *TrafficReadWriteCloser) Close() error {
return t.conn.Close() return t.conn.Close()
} }
func newTrafficReadWriteCloser(conn io.ReadWriteCloser, readCallback, writeCallback func(int)) io.ReadWriteCloser { // NewTrafficRWC wraps ReadWriteCloser to have read/write callbacks.
func NewTrafficRWC(conn io.ReadWriteCloser, readCallback, writeCallback func(int)) io.ReadWriteCloser {
return &TrafficReadWriteCloser{ return &TrafficReadWriteCloser{
conn: conn, conn: conn,
readCallback: readCallback, readCallback: readCallback,