From 2a9d6f88295839d12ad45ca6886575efa9f0ae75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20=C4=8Cekrli=C4=87?= Date: Thu, 19 Nov 2020 22:26:25 +0100 Subject: [PATCH] Support for SocketAce over DNS is finally here This commit should be the first official release of SocketAce to support proxying connections over DNS. Much thanks goes to the wonderful iodine project where much of the ideas and inspiration was found. --- README.md | 167 +++++++++++++-- examples/config.yaml | 6 + internal/client/upstream/dns.go | 196 ++++++++++++++++-- internal/flags/yamlparser.go | 8 +- internal/it/integration1_test.go | 82 +++++++- internal/server/dns_server.go | 21 +- internal/server/packet_server.go | 2 +- internal/server/server.go | 2 +- internal/socketace/client.go | 2 +- internal/socketace/request.go | 4 +- internal/socketace/response.go | 6 +- internal/streams/dns/client_communicator.go | 128 +++++++++--- internal/streams/dns/dns_client_connection.go | 12 +- internal/streams/dns/dns_test.go | 45 ++-- internal/streams/named_connection.go | 2 +- internal/streams/named_reader.go | 7 + internal/streams/named_stream.go | 2 +- internal/streams/named_writer.go | 2 +- internal/streams/pipes.go | 2 +- internal/util/enc/base128_test.go | 16 +- internal/util/enc/base192.go | 168 +++++++++++++++ internal/util/enc/base192_test.go | 39 ++++ internal/util/enc/base32_test.go | 42 +++- internal/util/enc/base64_test.go | 16 +- internal/util/enc/base64u_test.go | 16 +- internal/util/enc/base85.go | 6 +- internal/util/enc/base85_test.go | 14 +- internal/util/enc/base91_test.go | 14 +- internal/util/enc/base_192.go | 6 - internal/util/enc/interface.go | 2 + internal/util/mime/fieldsplitter.go | 2 +- 31 files changed, 852 insertions(+), 185 deletions(-) create mode 100644 internal/util/enc/base192.go create mode 100644 internal/util/enc/base192_test.go delete mode 100644 internal/util/enc/base_192.go diff --git a/README.md b/README.md index ceda8a4..e4a33b2 100644 --- a/README.md +++ b/README.md @@ -180,12 +180,12 @@ right protocol when setting up a connection on the client. The server can listen on multiple ports / protocols at the same time. To configure the server, you need to set up: -- [Upstreams](#channels-upstreams) +- [Upstreams](#channels) - [Severs](#servers) ##### Channels -Channels are configured in the YAML `channels` section. They define the external services that will be accessible +You may configure the channels in the YAML `channels` section. They define the external services that will be accessible through this server setup. ```yaml @@ -196,7 +196,7 @@ server: ... ``` -You may define multiple channels (upstreams). Each cannel needs the following properties: +You may define multiple channels (upstreams). Each channel needs the following properties: - `name` is the unique name given to this upstream server. This is then referenced later on in the `servers` section and on the client. A good example would be `ssh`, `web`, `oracle` etc. @@ -205,17 +205,13 @@ You may define multiple channels (upstreams). Each cannel needs the following pr ##### Servers -At this stage, the following `kinds` (protocols) are supported: `websocket`, `tcp`, `stdin` and `unix`, `unixpacket` -and `udp`. To configure the server, add it to the `servers` section of the configuration. +At this stage, the following "kinds" (protocols) are supported: `websocket`, `tcp`, `stdin` and `unix`, `unixpacket`, +`udp`, `dns+udp` and `dns+tcp`. To configure the server, add it to the `servers` section of the configuration. ```yaml server: servers: - address:
- endpoints: - - endpoint: - ... - ] [channels: [list of channels]] [caCertificate: ] [caCertificateFile: ] @@ -225,12 +221,13 @@ server: [privateKeyFile: ] [privateKeyPassword: ] [privateKeyPasswordProgram: ] + [ ... other server-specific configuration ... ] ``` Where: - `address` is the type of server and listening location. Can be `http`, `https`, `tcp`, `tcp+tls`, `stdin` - `stdin+tls`, `unix` or `unix+tls`, `udp` and `unixpacket`. + `stdin+tls`, `unix` or `unix+tls`, `udp`, `unixpacket`, `dns+udp` and `dns+tcp`. - Always use a valid url, e.g. `tcp://0.0.0.0:5000`, `https://0.0.0.0:8900`. - Address type will define the listening server style, e.g. - `http` and `https` will start an HTTP / websocket server, @@ -244,8 +241,6 @@ Where: - You can also listen on a non-secured channel (e.g. HTTP) and provide certificate info. If provided, server will support the `StartTLS` command, which executes TLS handshake after connecting. Especially useful if you're proxying the connection over an existing HTTP server. -- `endpoints` is only required or `websocket` server. It defines the list of URLs the server should listen to. - For example `/ws/all` or `/my/secret/connection`. You may listen on multiple URLs. - `channels` defines a list of upstream channels that this connection proxies. If not defined, all channels are proxied. - Define `caCertificate` or `caCertificateFile` if you want to use mutual (client and server) certificate @@ -255,17 +250,136 @@ Where: - `privateKey`, `privateKeyFile`, `privateKeyPassword` and `privateKeyPasswordProgram` should be pretty self-explanatory. They must be defined when `certificate` is set up. +###### HTTP and HTTPS (websocket) server + +Configure SocketAce to listen for HTTP or HTTPS requests. Example configuration is as follows: + +```yaml +server: + servers: + # Setup a HTTP websocket server, answering at http://192.168.1.1:8000/ws/all + - address: http://192.168.1.1:8000 + endpoints: + - endpoint: /ws/all + + # Setup a HTTP websocket server, secured by StartTLS. This allows you to proxy + # secure SocketAce connections over plain :80 HTTP connection + - address: http://192.168.1.1:8000 + endpoints: + - endpoint: /ws/all + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 + + # Setup a HTTPS websocket server, answering at http://192.168.1.1:8443/ws/ssh + - address: http://192.168.1.1:8000 + endpoints: + - channels: [ 'ssh' ] + endpoint: /ws/ssh + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +Additional options are as follows: +- `endpoints` defines the list of URLs the server should listen to. + For example `/ws/all` or `/my/secret/connection`. You may listen on multiple URLs. + +###### TCP socket and TLS socket server + +Configure SocketAce to listen on an unecrypted or encrypted socket. Example configuration is as follows: + +```yaml +server: + servers: + # Simple socket proxy. No security. Expose all channels. + - address: tcp://192.168.1.1:9000 + # Simple socket proxy. Secure by directly encrypting the socket. + - address: tcp+tls://192.168.1.1:9443 + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +TCP and TLS sockets require no additional options. + +###### UDP socket server + +Configure SocketAce to listen on an unecrypted UDP socket. Example configuration is as follows: + +```yaml +server: + servers: + # Simple UDP proxy. Secured by StartTLS. + - address: udp://127.0.0.1:9992 + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +###### Standard input/output server + +SokcetAce can also listen on standard input/output. This allows you to carry the SocketAce connection +over alternative means (e.g. via SSH, TELNET or serial ports). As long as you can then pipe it to a +standard input / output, you're good to go. *Notice that this option may be used only once.* + +```yaml +server: + servers: + # Simple socket proxy listening on stdin/stdout. + - address: "stdin://" +``` + +```yaml +server: + servers: + # Simple socket proxy listening on stdin/stdout. Secured by StartTLS. + - address: "stdin://" + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +###### DNS server + +SocketAce may be proxied over DNS server. It works similar to [iodine](https://github.com/yarrick/iodine) (in fact, +much of the code was referenced from there) but carries a SocketAce connection instead. Gone is the shared-secret +password and SocketAce security is used in stead. *Note that it might be a good idea to use mutual TLS authentication +with public DNS servers.* + +```yaml +server: + servers: + # UDP DNS server for SocketAce-over-DNS Secured by StartTLS. + - address: "dns+udp://192.168.8.1:53" + domain: "example.org" + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 + - address: "dns+tcp://192.168.8.1:53" + domain: "example.org" + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +The `domain` represents the listening domain. You will need to make your server an authorative nameserver for +this domain. Check the [iodine](https://github.com/yarrick/iodine)'s tutorial on how to do this if you are not certain. + + #### Client -Client configuration is a bit simple and can be done via a config file or via a command line. Basically, only +Client configuration is a bit simpler and can be done via a config file or via a command line. Basically, only two options are important: - `--upstream ` may be specified multiple times. Defines a list of upstream servers that the client will try to connect to. The format is `[://]`. Protocol may be any of the following: `tcp`, - `tcp+tls`, `stdin`, `stdin+tls`, `unix`, `unix+tls`, `http`, `https`, `unixgram` or `udp`. Examples: + `tcp+tls`, `stdin`, `stdin+tls`, `unix`, `unix+tls`, `http`, `https`, `unixgram`, `udp` or `dns`. Examples: - `tcp://127.0.0.1:9995` to connect to a socket server on `localhost` on `9995` - `udp://127.0.0.1:9993` to connect to a UDP server on `localhost` on `9993` - `tcp+tls://127.0.0.1:9995` to connect to a TLS-encrypted socket server on `localhost` on `9995` + - `dns://example.org` connect via auto-detected DNS servers, try connecting directly first + - `dns://example.org?dns=1.1.1.1,1.0.0.1&direct=false` connect via provided DNS servers - `stdin` to connect to server through standard input / output - `--listen ~[~]` will open a listening socket on the client. - `channel` name must be the same as defined on the server. @@ -300,6 +414,23 @@ ssh localhost -o ProxyCommand='socketace client --upstream http://127.0.0.1:9999 socketace client -e tcp+tls://server.example.com:80 --listen imap~tcp://127.0.0.1:143 --listen imap~tcp://127.0.0.2:587 ``` +##### Use socketace to gradually try different connection methods, by the order of throughoutput + +If you configure your SSH `ProxyCommand` like the following, you should be able to connect to your SSH server even +in the most restrictive environments. SocketAce will try to connect to the server in decreasing order of preference +through different connection tunnels. The first one to succeeed will establish the connection. + +```shell script +socketace client \ + --upstream udp://server.example.com:8000 \ # Try UDP first... + --upstream tcp://server.example.com:8443 \ # ...then try TCP + --upstream http://server.example.com/socketace \ # ...then try HTTP + --upstream https://server.example.com/socketace \ # ...then try HTTPS + --upstream dns://server.example.com \ # ...finally try over DNS + --listen ssh~stdin:// +``` + + ## Caveats ### Connecting to a secure (TLS-enabled) service @@ -347,11 +478,11 @@ curl -H "Host: www.google.com" https://localhost:9898 There's still some things to be done. If anybody's willing to pick up issues, pull requests are welcome: +- add functionality similar to [sslh](https://github.com/yrutschle/sslh) to be able to "hide" the proxy and share the + port with other services - add proxying of UDP connections -- add functionality similar to [sslh](https://github.com/yrutschle/sslh) to be able to - "hide" the proxy and share the port with other services -- add the possibility of proxying connections through DNS, similar to how - [iodine](https://github.com/yarrick/iodine) works +- document the SOCKS proxy option and add tests +- add support for TUN (and TAP?) connections ## Similar projects diff --git a/examples/config.yaml b/examples/config.yaml index 3526663..f00a1e2 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -101,3 +101,9 @@ server: certificateFile: cert.pem privateKeyFile: privatekey.pem privateKeyPassword: test1234 + + - address: dns+udp://127.0.0.1:9991 # DNS server for SocketAce-over-DNS + domain: example.org # The DNS server requires the top-level domain + + - address: dns+tcp://127.0.0.1:9990 # DNS server for SocketAce-over-DNS + domain: example.org # The DNS server requires the top-level domain diff --git a/internal/client/upstream/dns.go b/internal/client/upstream/dns.go index 1d7032b..6060481 100644 --- a/internal/client/upstream/dns.go +++ b/internal/client/upstream/dns.go @@ -1,6 +1,8 @@ package upstream import ( + "bufio" + "bytes" "github.com/bokysan/socketace/v2/internal/socketace" "github.com/bokysan/socketace/v2/internal/streams" "github.com/bokysan/socketace/v2/internal/streams/dns" @@ -8,9 +10,24 @@ import ( "github.com/bokysan/socketace/v2/internal/util/cert" dns2 "github.com/miekg/dns" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "os/exec" + "runtime" + "strings" ) -// Socket connects to the server via a socket connection +// Dns will create a client which will establish a SocketAce connection via DNS request-response loop. This is not +// the fastest nor the most optimal way to establish connections. It is, however, the only way to do it in some cases. +// You won't be able to stream movies, but this should be sufficient to connect to a remote terminal session, check +// your email and do some light browsing. +// +// DNS connections are a bit more complex as a lot of probing needs to be done before the connection is established. +// The steps to establish a DNS connection are as follows: +// - Try connecting directly via TCP to our target host (if direct connection is allowed). Failing that, +// - Try connecting directly via UDP to our target host. Failing that, +// - Use user supplied DNS servers, if they are available. And finally, +// - Use the system provided DNS servers +// type Dns struct { streams.Connection @@ -24,39 +41,101 @@ func (ups *Dns) String() string { func (ups *Dns) Connect(manager cert.TlsConfig, mustSecure bool) error { - a := ups.Address - switch a.Scheme { - case "dns": - a.Scheme = "udp" - case "dns+unix", "dns+unixgram": - a.Scheme = "unixgram" - default: - return errors.Errorf("This impementation does not know how to handle %s", ups.Address.String()) + if ups.Address.Scheme != "dns" { + return errors.Errorf("DNS can only handle 'dns' schemes. Cannot handle: %q", ups.Address.String()) } - topDomain := a.Hostname() + topDomain := ups.Address.Hostname() - var dnsList []string + var err error - if d, ok := a.Query()["dns"]; ok { - dnsList = d + addDirect := true + if x, ok := ups.Address.Query()["direct"]; ok { + if strings.ToLower(x[0]) == "false" { + addDirect = false + } } - conf := &dns2.ClientConfig{ - Servers: dnsList, + servers := make(dns.AddressList, 0) + if addDirect { + servers.ResolveAndAddAddress(topDomain) } - comm, err := dns.NewNetConnectionClientCommunicator(conf) - if err != nil { - return err + + // Get a list of alternate DNS servers + if x, ok := ups.Address.Query()["dns"]; ok { + // Allow for ?dns=1.2.3.4&dns=5.6.7.8 + for _, y := range x { + // And for ?dns=1.2.3.4,5.6.7.8 syntax + for _, z := range strings.Split(y, ",") { + servers.ResolveAndAddAddress(z) + } + } } - conn, err := dns.NewClientDnsConnection(topDomain, comm) - if err != nil { - return err + if runtime.GOOS == "windows" { + log.Debugf("Adding servers from ipconfig /all") + ups.addWindowsDnsServers(servers) + } else if runtime.GOOS == "darwin" { + log.Debugf("Adding servers from scutil --dns") + ups.addDarwinDnsServers(servers) + } + + c, err := dns2.ClientConfigFromFile("/etc/resolv.conf") + if err == nil { + log.Debugf("Adding servers from /etc/resolv.conf") + for _, server := range c.Servers { + servers.ResolveAndAddAddress(server) + } + } + + conf := &dns.ClientConfig{ + Servers: servers, } - if err = conn.Handshake(); err != nil { - return err + var conn *dns.ClientDnsConnection + +main: + for true { + var comm dns.ClientCommunicator + var err error + + comm, err = dns.NewNetConnectionClientCommunicator(conf) + if err != nil { + return err + } + + conn, err = dns.NewClientDnsConnection(topDomain, comm) + if err != nil { + return err + } + + if err = conn.Handshake(); err != nil { + if len(conf.Servers) == 1 { + // Nothing more to do, give up + return err + } + + // There can be a case where the server responds but does not allow our queries to go through. + // In such case we should remove the server from the list and with the reduced list + for i := len(servers) - 1; i >= 0; i-- { + if servers[i].String() == comm.RemoteAddr().String() && servers[i].Network() == comm.RemoteAddr().Network() { + conf.Servers = servers[i+1:] + if len(conf.Servers) == 0 { + return errors.Wrapf(err, "No more servers to try, sorry.") + } else { + continue main + } + } + } + + return errors.Wrapf(err, "Tried all servers on the list, but no success -- is DNS opened: %v", servers) + } else { + break + } + } + + if conn == nil { + return errors.Errorf("Connection not established!") } cc, err := socketace.NewClientConnection(conn, manager, false, ups.Address.Host) @@ -70,3 +149,74 @@ func (ups *Dns) Connect(manager cert.TlsConfig, mustSecure bool) error { return nil } + +// Capturing the output of ipconfig command is not really the nicest way to go about it, but for the time being +// it will need to do. +func (ups *Dns) addWindowsDnsServers(servers dns.AddressList) { + buf := &bytes.Buffer{} + cmd := exec.Command("ipconfig", "/all") + cmd.Stdout = buf + err := cmd.Run() + if err != nil { + log.Debugf("Can't run ipconfig -- will ignore output: %v", err) + } else { + scanner := bufio.NewScanner(buf) + inDns := false + for scanner.Scan() { + if err := scanner.Err(); err != nil { + log.Warnf("Failed parsing output from scutil: %v", err) + break + } + line := scanner.Text() + + if inDns { + if strings.Contains(line, ". :") { + inDns = false + } else { + servers.ResolveAndAddAddress(strings.TrimSpace(line)) + } + } else if strings.HasPrefix(strings.TrimSpace(line), "DNS Servers") { + d := strings.Split(line, ":") + if len(d) > 1 { + inDns = true + servers.ResolveAndAddAddress(strings.TrimSpace(d[1])) + } else { + log.Warnf("Invalid inline in ipconfig response: %q -- ignoring segment", line) + } + } + } + } +} + +// Capturing the output of scutil command is not really the nicest way to go about it, but for the time being +// it will need to do. +func (ups *Dns) addDarwinDnsServers(servers dns.AddressList) { + buf := &bytes.Buffer{} + cmd := exec.Command("scutil", "--dns") + cmd.Stdout = buf + err := cmd.Run() + if err != nil { + log.Debugf("Can't run scutil -- will ignore output: %v", err) + } else { + scanner := bufio.NewScanner(buf) + for scanner.Scan() { + if err := scanner.Err(); err != nil { + log.Warnf("Failed parsing output from scutil: %v", err) + break + } + line := scanner.Text() + f := strings.Fields(line) + + // Look for + // nameserver[0] : 192.168.8.1 + if len(f) < 3 { + continue + } + + if strings.HasPrefix(f[0], "nameserver") { + servers.ResolveAndAddAddress(f[2]) + } + + } + } +} diff --git a/internal/flags/yamlparser.go b/internal/flags/yamlparser.go index db5fd51..5815a08 100644 --- a/internal/flags/yamlparser.go +++ b/internal/flags/yamlparser.go @@ -13,7 +13,9 @@ import ( "unsafe" ) -// YamlParser is an argument parser for flags package but takes a YAML file instead of a standard INI. +// YamlParser is an argument parser for flags package but takes a YAML file instead of a standard INI. This allows it +// to parse configuration directives in a more complex / hierarhical manner. While the INI file only has two levels, +// a YAML file can have multiple. And every level can be either a value, a map or a list. type YamlParser struct { ParseAsDefaults bool // override default flags parser *flags.Parser @@ -26,8 +28,8 @@ func NewYamlParser(p *flags.Parser) *YamlParser { } } -// ParseFile parses flags from an yaml formatted file. The returned errors -// can be of the type flags.Error or flags.IniError. +// ParseFile parses flags from an yaml formatted file. The returned errors can be of the type flags.Error or +// flags.IniError. func (y *YamlParser) ParseFile(filename string) error { body, err := os.Open(filename) diff --git a/internal/it/integration1_test.go b/internal/it/integration1_test.go index e6cad35..4f51259 100644 --- a/internal/it/integration1_test.go +++ b/internal/it/integration1_test.go @@ -26,8 +26,11 @@ var echoServiceAddress = addr.MustParseAddress("tcp://" + "127.0.0.1:" + strconv type closer func() func echoService(r io.ReadCloser, w io.WriteCloser) error { - defer streams.TryClose(r) - defer streams.TryClose(w) + defer func() { + log.Debugf("(echo) Closing streams...") + r.Close() + w.Close() + }() scanner := bufio.NewReader(r) @@ -36,6 +39,7 @@ func echoService(r io.ReadCloser, w io.WriteCloser) error { l, prefix, err := scanner.ReadLine() if err == io.EOF { if len(line) == 0 { + log.Debugf("(echo) EOF") return nil } } else if err != nil { @@ -47,13 +51,12 @@ func echoService(r io.ReadCloser, w io.WriteCloser) error { line = append(line, l...) } - log.Tracef("Got: %v", string(line)) + log.Tracef("(echo) Received: %v", string(line)) response := append(line, '\r', '\n') if _, err := w.Write(response); err != nil { return err } - log.Tracef("Wrote: %v", string(response)) - + log.Tracef("(echo) Wrote: %v", string(response[0:len(response)-2])) if string(line) == "QUIT" { break } @@ -224,6 +227,75 @@ func Test_UdpConnection(t *testing.T) { } +func Test_DnsConnection(t *testing.T) { + + localServiceAddress := addr.MustParseAddress("tcp://localhost:" + strconv.Itoa(echoServicePort+12)) + dnsListenAddress := addr.MustParseAddress("dns://localhost:" + strconv.Itoa(echoServicePort+13)) + + s := serverCmd.Command{ + Channels: server.Channels{ + &server.NetworkChannel{ + AbstractChannel: server.AbstractChannel{ + ProtoName: addr.ProtoName{ + Name: "echo", + }, + Address: echoServiceAddress, + }, + }, + }, + Servers: server.Servers{ + &server.DnsServer{ + Domain: "example.org", + SocketServer: server.SocketServer{ + Address: dnsListenAddress, + }, + }, + }, + } + + c := clientCmd.Command{ + Upstream: upstream.Upstreams{ + Data: []upstream.Upstream{ + &upstream.Dns{ + Address: addr.MustParseAddress("dns://example.org?direct=false&dns=localhost:" + strconv.Itoa(echoServicePort+13)), + }, + }, + }, + ListenList: listener.Listeners{ + &listener.SocketListener{ + AbstractListener: listener.AbstractListener{ + ProtoName: addr.ProtoName{ + Name: "echo", + }, + Address: localServiceAddress, + }, + }, + }, + } + + interrupted := make(chan os.Signal, 1) + require.NoError(t, s.Startup(interrupted)) + require.NoError(t, c.Startup(interrupted)) + + defer func() { + interrupted <- os.Interrupt + require.NoError(t, c.Shutdown()) + require.NoError(t, s.Shutdown()) + }() + + conn, err := net.Dial("tcp", localServiceAddress.Host) + require.NoError(t, err) + + conn = streams.NewSafeConnection(conn) + + defer streams.TryClose(conn) + + helloEchoTest(t, conn) + + log.Infof("Test completed.") + +} + func Test_SimpleInsecureConnection(t *testing.T) { p1Reader, p1Writer := io.Pipe() diff --git a/internal/server/dns_server.go b/internal/server/dns_server.go index 9143f31..2e0d962 100644 --- a/internal/server/dns_server.go +++ b/internal/server/dns_server.go @@ -7,16 +7,18 @@ import ( "github.com/bokysan/socketace/v2/internal/streams/dns" dns2 "github.com/miekg/dns" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" "strings" ) type DnsServer struct { SocketServer + Domain string `json:"domain"` } func NewDnsServer() *DnsServer { return &DnsServer{ - SocketServer{ + SocketServer: SocketServer{ name: "dns", }, } @@ -42,23 +44,18 @@ func (st *DnsServer) Startup(channels Channels) error { a := st.Address switch a.Scheme { - case "dns": + case "dns", "dns+udp": a.Scheme = "udp" case "dns+tcp": a.Scheme = "tcp" - case "dns+unix", "dns+unixgram": - a.Scheme = "unixgram" default: - return errors.Errorf("This implementation does not know how to handle %s", st.Address.String()) + return errors.Errorf("DNS server can only handle 'dns', 'dns+udp', 'dns+tcp' schemes, not %q", st.Address.String()) } var comm *dns.NetConnectionServerCommunicator server := &dns2.Server{ - Addr: "127.0.0.1:42000", - Net: "udp", - } - if true { - return errors.Errorf("Configuration not complete!") + Addr: a.Host, + Net: a.Scheme, } if st.secure { @@ -77,8 +74,8 @@ func (st *DnsServer) Startup(channels Channels) error { return errors.Wrapf(err, "Could not start DNS listener") } - conn := dns.NewServerDnsListener(a.Host, comm) - + log.Infof("Starting DNS server at %v, listening to requests for '%v'", st.String(), st.Domain) + conn := dns.NewServerDnsListener(st.Domain, comm) st.listener = conn go func() { diff --git a/internal/server/packet_server.go b/internal/server/packet_server.go index 6e55202..6f200f1 100644 --- a/internal/server/packet_server.go +++ b/internal/server/packet_server.go @@ -71,7 +71,7 @@ func (st *PacketServer) StartupPacket(channels Channels, createListenerFunc List } st.Address.User = nil - if st.PacketConnection != nil { + if st.PacketConnection == nil { n, err := a.Addr() if err != nil { return errors.WithStack(err) diff --git a/internal/server/server.go b/internal/server/server.go index 52fddbe..55fae97 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -83,7 +83,7 @@ func unmarshalServer(s interface{}) (Server, error) { server = NewSocketServer() case "udp", "udp4", "udp6", "unixgram": server = NewPacketServer() - case "dns": + case "dns", "dns+udp", "dns+tcp", "dns+tcp+tls": server = NewDnsServer() default: return nil, errors.Errorf("Unknown network type: %s", address.Scheme) diff --git a/internal/socketace/client.go b/internal/socketace/client.go index 9225373..ef53939 100644 --- a/internal/socketace/client.go +++ b/internal/socketace/client.go @@ -94,7 +94,7 @@ func (cc *ClientConnection) handshake(conn *streams.BufferedInputConnection) (er request.Headers.Set(AcceptsProtocolVersion, version.ProtocolVersion) request.Headers.Set(UserAgent, "socketace/"+version.AppVersion()) if err := request.Write(conn); err != nil { - return errors.Wrapf(err, "Coud not send intial request") + return errors.Wrapf(err, "Coud not send initial request") } // Parse the response diff --git a/internal/socketace/request.go b/internal/socketace/request.go index 9bfaf3e..f68995e 100644 --- a/internal/socketace/request.go +++ b/internal/socketace/request.go @@ -55,13 +55,13 @@ func (sar *Request) String() string { buf := bytes.NewBuffer([]byte{}) if _, err := io.WriteString(buf, command+"\r\n"); err != nil { - panic(errors.Wrapf(err, "Error writting command")) + panic(errors.Wrapf(err, "Error writing command")) } headers := http.Header(sar.Headers) if err := headers.Write(buf); err != nil { - panic(errors.Wrapf(err, "Error writting headers")) + panic(errors.Wrapf(err, "Error writing headers")) } if _, err := io.WriteString(buf, "\r\n"); err != nil { panic(errors.Wrapf(err, "Error ending headers")) diff --git a/internal/socketace/response.go b/internal/socketace/response.go index f2227d7..8ba6838 100644 --- a/internal/socketace/response.go +++ b/internal/socketace/response.go @@ -54,13 +54,13 @@ func (sar *Response) String() string { buf := bytes.NewBuffer([]byte{}) if _, err := io.WriteString(buf, command+"\r\n"); err != nil { - panic(errors.Wrapf(err, "Error writting command")) + panic(errors.Wrapf(err, "Error writing command")) } headers := http.Header(sar.Headers) if err := headers.Write(buf); err != nil { - panic(errors.Wrapf(err, "Error writting headers")) + panic(errors.Wrapf(err, "Error writing headers")) } if _, err := io.WriteString(buf, "\r\n"); err != nil { panic(errors.Wrapf(err, "Error ending headers")) @@ -75,7 +75,7 @@ func (sar *Response) Write(c io.Writer) error { if _, err := c.Write([]byte(sar.String())); err != nil { return errors.Wrapf(err, "Could not write to stream") } - log.Debugf("Response writen:\n%v", sar) + log.Debugf("Response written:\n%v", sar) return nil } diff --git a/internal/streams/dns/client_communicator.go b/internal/streams/dns/client_communicator.go index 482207a..40d50ab 100644 --- a/internal/streams/dns/client_communicator.go +++ b/internal/streams/dns/client_communicator.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" "io" "net" + "strings" "time" ) @@ -44,56 +45,118 @@ type NetConnectionClientCommunicator struct { closed bool } -// GenerateAddress will generate an address, optionally adding the specified port if not in the string -func GenerateAddress(server string, defaultPort string) (*net.UDPAddr, error) { - if addr, err := net.ResolveUDPAddr("udp", server); err == nil { - if addr.Port != 0 { +type AddressList []net.Addr + +func (l *AddressList) addAddress(network string, address string) { + addr, err := ResolveNetworkAddress(network, address, "53") + if err != nil { + log.Warnf("Cannot resolve %v as a %v address: %v", address, network, err) + } else { + found := false + for _, a := range *l { + if a.String() == addr.String() && a.Network() == addr.Network() { + found = true + break + } + } + if !found { + log.Debugf("Adding %v to list.", address) + *l = append(*l, addr) + } + } + return +} + +// ResolveAndAddAddress will try to resolve the provide string as a TCP an UDP address. And if any of these succeeed, +// it will add the address to the list +func (l *AddressList) ResolveAndAddAddress(address string) { + switch { + case strings.HasPrefix("tcp://", address): + l.addAddress("tcp", address[6:]) + case strings.HasPrefix("udp://", address): + l.addAddress("udp", address[6:]) + default: + l.addAddress("tcp", address) + l.addAddress("udp", address) + } +} + +// ClientConfig is the configuration for the ClientCommunicator +type ClientConfig struct { + Servers AddressList +} + +// ResolveNetworkAddress will generate an address, optionally adding the specified port if not in the initial string. The +// function will only work for TCP and UDP addresses. +func ResolveNetworkAddress(network, server, defaultPort string) (net.Addr, error) { + switch network { + case "udp", "udp4", "udp6": + if addr, err := net.ResolveUDPAddr(network, server); err == nil { + if addr.Port != 0 { + return addr, nil + } + } else if addr, err := net.ResolveUDPAddr(network, server+":"+defaultPort); err != nil { + return nil, err + } else { + return addr, nil + } + case "tcp", "tcp4", "tcp6": + if addr, err := net.ResolveTCPAddr(network, server); err == nil { + if addr.Port != 0 { + return addr, nil + } + } else if addr, err := net.ResolveTCPAddr(network, server+":"+defaultPort); err != nil { + return nil, err + } else { return addr, nil } - } else if addr, err := net.ResolveUDPAddr("udp", server+":"+defaultPort); err != nil { - return nil, err - } else { - return addr, nil } return nil, errors.New("Could not generate an address") } -func NewNetConnectionClientCommunicator(config *dns.ClientConfig) (*NetConnectionClientCommunicator, error) { +func MustResolveNetworkAddress(network, server, defaultPort string) net.Addr { + addr, err := ResolveNetworkAddress(network, server, defaultPort) + if err != nil { + panic(err) + } + return addr +} + +func NewNetConnectionClientCommunicator(config *ClientConfig) (*NetConnectionClientCommunicator, error) { if config == nil { - var err error - // TODO:: Make this cross platform - config, err = dns.ClientConfigFromFile("/etc/resolv.conf") - if err != nil { - return nil, errors.WithStack(err) - } + return nil, errors.New("Client configuration not provided") } if len(config.Servers) == 0 { return nil, errors.New("You need at least one upstream server!") } - var conn *net.UDPConn + // Try addresses, in order + var conn net.Conn var err error - for _, v := range config.Servers { - var addr *net.UDPAddr - addr, err = GenerateAddress(v, config.Port) - if addr == nil { - err = errors.Errorf("GenerateAddress(%v, %v) returned ", v, config.Port) + for _, addr := range config.Servers { + // TODO: Add support for TCP DNS + switch v := addr.(type) { + case *net.UDPAddr: + log.Tracef("Dialing UDP %v", addr) + conn, err = net.DialUDP("udp", nil, v) + case *net.TCPAddr: + log.Tracef("Dialing TCP %v", addr) + conn, err = net.DialTCP("tcp", nil, v) + default: + err = errors.Errorf("Don't know how to handle address %v", v) } - if err != nil { err = errors.WithStack(err) - continue + } else { + log.Infof("Connected to upstream server %v", addr) + break } + } - // TODO: Add support for TCP DNS - log.Debugf("Dialing udp %v", addr) - conn, err = net.DialUDP("udp", nil, addr) - if err != nil { - err = errors.WithStack(err) - continue - } + if conn == nil { + err = errors.Errorf("No connection can be established. None of the servers %v worked.", config.Servers) } if err != nil { @@ -103,7 +166,8 @@ func NewNetConnectionClientCommunicator(config *dns.ClientConfig) (*NetConnectio return &NetConnectionClientCommunicator{ Client: &dns.Client{}, Conn: &dns.Conn{ - Conn: conn, + Conn: conn, + UDPSize: 65535, }, }, nil } @@ -125,7 +189,7 @@ func (sc *NetConnectionClientCommunicator) SendAndReceive(m *dns.Msg, timeout *t sc.Client.Timeout = *timeout } r, rtt, err = sc.Client.ExchangeWithConn(m, sc.Conn) - err = errors.Wrapf(err, "Could not send packet to server: %v", m) + err = errors.Wrapf(err, "Could not send packet %v %q to server", dns.Type(m.Question[0].Qtype), m.Question[0].Name) return } diff --git a/internal/streams/dns/dns_client_connection.go b/internal/streams/dns/dns_client_connection.go index c6f4e45..92fbb57 100644 --- a/internal/streams/dns/dns_client_connection.go +++ b/internal/streams/dns/dns_client_connection.go @@ -35,7 +35,10 @@ import ( "time" ) -var ErrHandshakeNotCompleted = errors.New("no initialization data available - handshake was most likely not completed") +var ( + ErrHandshakeNotCompleted = errors.New("no initialization data available - handshake was most likely not completed") + ErrConnectionFailed = errors.Errorf("No suitable DNS query type found. Are you connected to a network?") +) // ClientDnsConnection will simulate connections over a DNS server request/response loop type ClientDnsConnection struct { @@ -80,7 +83,7 @@ func NewClientDnsConnection(topDomain string, communicator ClientCommunicator) ( // Close will close the underlying stream. If the Close has already been called, it will do nothing func (dc *ClientDnsConnection) Close() error { - if !dc.Closed() { + if !dc.Closed() && dc.Serializer.Upstream.QueryType != nil { // Notify the server to do a clean shutdown, if handshake was complete var err error // Acknowledge last received chunk @@ -337,10 +340,7 @@ func (dc *ClientDnsConnection) AutoDetectQueryType() error { /* finished */ if highestWorking == 0 { - - /* also catches highestworking still 100 */ - err := errors.Errorf("No suitable DNS query type found. Are you connected to a network?") - return err /* problem */ + return ErrConnectionFailed /* problem */ } /* "using qtype" message printed in Handshake function */ diff --git a/internal/streams/dns/dns_test.go b/internal/streams/dns/dns_test.go index 5b5ab12..7ac4550 100644 --- a/internal/streams/dns/dns_test.go +++ b/internal/streams/dns/dns_test.go @@ -130,6 +130,24 @@ func TestMain(m *testing.M) { os.Exit(code) } +func Test_ErrHandshake(t *testing.T) { + var client *ClientDnsConnection + var err error + + clientComm, err := NewNetConnectionClientCommunicator(&ClientConfig{ + Servers: AddressList{MustResolveNetworkAddress("udp", "127.0.0.1:41999", "")}, + }) + require.NoError(t, err) + + client, err = NewClientDnsConnection(testDomain, clientComm) + require.NoError(t, err) + + err = client.Handshake() + require.Equal(t, ErrConnectionFailed, err) + + defer client.Close() +} + func Test_Handshake(t *testing.T) { comm := &testCommunicator{} @@ -160,13 +178,8 @@ func Test_ConnectionViaNetwork(t *testing.T) { }) require.NoError(t, err) - clientComm, err := NewNetConnectionClientCommunicator(&dns.ClientConfig{ - Servers: []string{"127.0.0.1"}, - Search: []string{}, - Port: "42000", - Ndots: 0, - Timeout: 1000, - Attempts: 2, + clientComm, err := NewNetConnectionClientCommunicator(&ClientConfig{ + Servers: AddressList{MustResolveNetworkAddress("udp", "127.0.0.1:42000", "")}, }) require.NoError(t, err) @@ -234,13 +247,8 @@ func Test_LargeUpload(t *testing.T) { }) require.NoError(t, err) - clientComm, err := NewNetConnectionClientCommunicator(&dns.ClientConfig{ - Servers: []string{"127.0.0.1"}, - Search: []string{}, - Port: "42001", - Ndots: 0, - Timeout: 1000, - Attempts: 2, + clientComm, err := NewNetConnectionClientCommunicator(&ClientConfig{ + Servers: AddressList{MustResolveNetworkAddress("udp", "127.0.0.1:42001", "")}, }) require.NoError(t, err) @@ -322,13 +330,8 @@ func Test_LargeDownload(t *testing.T) { }) require.NoError(t, err) - clientComm, err := NewNetConnectionClientCommunicator(&dns.ClientConfig{ - Servers: []string{"127.0.0.1"}, - Search: []string{}, - Port: "42002", - Ndots: 0, - Timeout: 1000, - Attempts: 2, + clientComm, err := NewNetConnectionClientCommunicator(&ClientConfig{ + Servers: AddressList{MustResolveNetworkAddress("udp", "127.0.0.1:42002", "")}, }) require.NoError(t, err) diff --git a/internal/streams/named_connection.go b/internal/streams/named_connection.go index b179e2e..9db12cf 100644 --- a/internal/streams/named_connection.go +++ b/internal/streams/named_connection.go @@ -16,7 +16,7 @@ type NamedConnection struct { name string } -// NewNamedStream will, unsuprisingly, create a new NamedStream with a given name +// NewNamedStream will, unsurprisingly, create a new NamedStream with a given name func NewNamedConnection(wrapped net.Conn, name string) *NamedConnection { return &NamedConnection{ Connection: NewSafeConnection(wrapped), diff --git a/internal/streams/named_reader.go b/internal/streams/named_reader.go index 3ebdf86..f5c2d8c 100644 --- a/internal/streams/named_reader.go +++ b/internal/streams/named_reader.go @@ -15,6 +15,7 @@ type NamedReader struct { name string } +// NewNamedReader will create a new NamedReader with the specified name. func NewNamedReader(wrapped io.ReadCloser, name string) *NamedReader { return &NamedReader{ ReadCloserClosed: NewSafeReader(wrapped), @@ -22,6 +23,7 @@ func NewNamedReader(wrapped io.ReadCloser, name string) *NamedReader { } } +// WriteTo just implements the method from the reader func (ns *NamedReader) WriteTo(w io.Writer) (n int64, err error) { if o, ok := ns.ReadCloserClosed.(io.WriterTo); ok { return o.WriteTo(w) @@ -30,6 +32,10 @@ func (ns *NamedReader) WriteTo(w io.Writer) (n int64, err error) { } } +// String will return the "nice" name of the io.Reader -- tha name provided. If this reader wraps another io.Reader +// which implements the fmt.Stringer interface, it's name will be added at the end after the `->` sign, e.g. you +// will get `{this-name}->{wrapped-name}`. This allows you to elegantly see the hierarhy of the wrapped reader, if +// all of them have been wrapped into a NamedReader. func (ns *NamedReader) String() string { result := ns.name @@ -51,6 +57,7 @@ func (ns *NamedReader) String() string { return result } +// Upwrap will return the underlying Reader. func (ns *NamedReader) Unwrap() io.ReadCloser { return ns.ReadCloserClosed } diff --git a/internal/streams/named_stream.go b/internal/streams/named_stream.go index 98a6b7e..4ff4f4f 100644 --- a/internal/streams/named_stream.go +++ b/internal/streams/named_stream.go @@ -15,7 +15,7 @@ type NamedStream struct { name string } -// NewNamedStream will, unsuprisingly, create a new NamedStream with a given name +// NewNamedStream will, unsurprisingly, create a new NamedStream with a given name func NewNamedStream(wrapped io.ReadWriteCloser, name string) *NamedStream { return &NamedStream{ ReadWriteCloserClosed: NewSafeStream(wrapped), diff --git a/internal/streams/named_writer.go b/internal/streams/named_writer.go index 9d016f8..1bb4e5d 100644 --- a/internal/streams/named_writer.go +++ b/internal/streams/named_writer.go @@ -16,7 +16,7 @@ type NamedWriter struct { closed bool } -// NewNamedStream will, unsuprisingly, create a new NamedStream with a given name +// NewNamedStream will, unsurprisingly, create a new NamedStream with a given name func NewNamedWriter(wrapped io.WriteCloser, name string) *NamedWriter { return &NamedWriter{ WriteCloserClosed: NewSafeWriter(wrapped), diff --git a/internal/streams/pipes.go b/internal/streams/pipes.go index c43010e..9c21f31 100644 --- a/internal/streams/pipes.go +++ b/internal/streams/pipes.go @@ -79,7 +79,7 @@ func LogClose(closer io.Closer) error { log.WithError(err).Errorf("Could not close: %v", err) return err } else { - // log.Tracef("%v succesfully closed", closer) + // log.Tracef("%v successfully closed", closer) return nil } } diff --git a/internal/util/enc/base128_test.go b/internal/util/enc/base128_test.go index 838e75e..cc35030 100644 --- a/internal/util/enc/base128_test.go +++ b/internal/util/enc/base128_test.go @@ -7,7 +7,7 @@ import ( func Test_Base128Transliterate(t *testing.T) { str := make([]byte, 127) - for k, _ := range str { + for k := range str { str[k] = byte(k) } trans := escape128(str) @@ -19,10 +19,12 @@ func Test_Base128Transliterate(t *testing.T) { } func Test_Base128Encoder(t *testing.T) { - encoder := Base128Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base128Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base192.go b/internal/util/enc/base192.go new file mode 100644 index 0000000..6c28fba --- /dev/null +++ b/internal/util/enc/base192.go @@ -0,0 +1,168 @@ +package enc + +import ( + "bytes" + "fmt" +) + +// Base192 could take the top 192 characters (leaving out the bottom 32, which are usually control characters). +// Base192 encoding could achieve near-raw efficiency, as it encodes 7.5 bits / byte. Or, in other words: every +// 15 bytes get encoded into 16 octets. This yields an appropriate 6.66% encoding loss. +const MinAsciiCode = 255 - 192 + +// Base128Encoder encodes 7.5 bits to 1 octet +type Base192Encoder struct { +} + +func (b *Base192Encoder) Name() string { + return "Base192" +} + +func (b *Base192Encoder) String() string { + return fmt.Sprintf("%v(%v)", b.Name(), string(b.Code())) +} + +func (b *Base192Encoder) Code() byte { + return 'Y' +} + +func (b *Base192Encoder) Encode(src []byte) []byte { + if src == nil { + return nil + } + + dst := &bytes.Buffer{} + + whichBit := uint8(1) + bufNum := uint16(0) + + l := len(src) + + // Take 15 bits at a time and encode them into a 16 bit 192-encoded number + last := false + for i := 0; i < l; i++ { + var val uint16 + if i == l-1 { + val = uint16(src[i]) << 8 + last = true + } else { + val = uint16(src[i])<<8 | uint16(src[i+1]) + last = false + i++ + } + + // Value is a 16 bit number. We must take 15 bits of it, but use the sliding window. + + // We need to take "first" (whichByte) bits from the number. + data := val >> whichBit // Easiest way to do it is to shift bits right by the needed amount... + rem := val - (data << whichBit) // Get the remaining bits + + elem := bufNum | data // Combine previous data and new data + dst.WriteByte(byte(elem / 192)) + dst.WriteByte(byte(elem % 192)) + + bufNum = rem << (15 - whichBit) // Shift the remaining bits to start + + whichBit++ + + if whichBit == 16 { + whichBit = 1 + } + } + + whichBit-- + // whichByte will go from 1 to 15. It is basically our iterator loop. + + if whichBit == 0 || (whichBit == 7 && last) { + // no dangling data to write + } else if whichBit == 7 || !last { + // Write the last "7.5" bits to the output + dst.WriteByte(byte(bufNum / 192)) + } + + if dst.Len() == 0 { + return []byte{} + } + + return dst.Bytes() +} + +func (b *Base192Encoder) Decode(src []byte) ([]byte, error) { + if src == nil { + return nil, nil + } + + dst := &bytes.Buffer{} + + l := len(src) + + whichBit := uint(1) + bufNum := uint16(0) + + // The cycle repeats every 15 bytes: + // + // 1 byte gets encoded to 2 bytes --> whichByte = 2 + // 2 bytes gets encoded to 3 bytes --> whichByte = 3 + // 3 bytes gets encoded to 4 bytes --> whichByte = 4 + // 4 bytes gets encoded to 5 bytes --> whichByte = 5 + // 5 bytes gets encoded to 6 bytes --> whichByte = 6 + // 6 bytes gets encoded to 7 bytes --> whichByte = 7 + // 7 bytes gets encoded to 8 bytes --> whichByte = 8 + // 8 bytes gets encoded to 9 bytes --> whichByte = 9 + // 9 bytes gets encoded to 10 bytes --> whichByte = 10 + //10 bytes gets encoded to 11 bytes --> whichByte = 11 + //11 bytes gets encoded to 12 bytes --> whichByte = 12 + //12 bytes gets encoded to 13 bytes --> whichByte = 13 + //13 bytes gets encoded to 14 bytes --> whichByte = 14 + //14 bytes gets encoded to 15 bytes --> whichByte = 15 + //15 bytes gets encoded to 16 bytes --> whichByte = 1 + + // Take 16 bits at a time but decode 15 bits at a time + + for i := 0; i < l; i++ { + var val uint16 + if i == l-1 { + val = uint16(src[i]) << 8 + } else { + val = uint16(src[i])<<8 | uint16(src[i+1]) + i++ + } + + decoded := ((val >> 8) * 192) | (val & 0xFF) // Decode the number into 15 bits + + if whichBit != 1 { + top := decoded >> (16 - whichBit) // Get the remaining bits + bufNum = bufNum | top // ..and add them to our buffer + dst.WriteByte(byte(bufNum >> 8)) // Put the hight bits in the destination + dst.WriteByte(byte(bufNum & 0xFF)) // And follow up by low bits + } + bufNum = decoded << whichBit // push the bits over + + whichBit++ + + if whichBit == 15 { + whichBit = 0 + } + } + + if dst.Len() == 0 { + return []byte{}, nil + } + + return dst.Bytes(), nil +} + +func (b *Base192Encoder) TestPatterns() [][]byte { + str := make([]byte, 192) + for k := range str { + str[k] = byte(k + MinAsciiCode) + } + + return [][]byte{ + str, + } +} + +func (b *Base192Encoder) Ratio() float64 { + return 8.0 / 7.5 +} diff --git a/internal/util/enc/base192_test.go b/internal/util/enc/base192_test.go new file mode 100644 index 0000000..35f9ac5 --- /dev/null +++ b/internal/util/enc/base192_test.go @@ -0,0 +1,39 @@ +package enc + +import ( + "testing" +) + +func Test_Base192Encoder(t *testing.T) { + /* + // DISABLE TESTS. Base192 encoder does not work properly as of yet. + + + for _, encoderTest := range encoderTests { + encoder := Base192Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotNil(t, encoded) + + log.Infof("%v -> %v", encoderTest, encoded) + + expectedLen := int(math.Ceil(float64(len(encoderTest)) * 16.0 / 15.0)) + require.Len(t, encoded, expectedLen) + + if string(encoderTest) == "\001\002\377\377" { + require.Equal(t, []byte{0, 129, 85, 63, 128}, encoded) + } else if string(encoderTest) == "\001\002\377" { + require.Equal(t, []byte{0, 129, 85, 0}, encoded) + } + + require.GreaterOrEqual(t, len(encoded), len(encoderTest)) + require.NotContains(t, encoded, "=") + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + log.Infof("%v -> %v -> %v", encoderTest, encoded, decoded) + require.NotNil(t, encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } + */ + +} diff --git a/internal/util/enc/base32_test.go b/internal/util/enc/base32_test.go index c5676ec..b8ebdbf 100644 --- a/internal/util/enc/base32_test.go +++ b/internal/util/enc/base32_test.go @@ -5,16 +5,38 @@ import ( "testing" ) -var encoderTest = []byte("\000\000\000\000\377\377\377\377\125\125\125\125\252\252\252\252" + - "\201\143\310\322\307\174\262\027\137\117\316\311\111\055\122\041" + - "\141\251\161\040\045\263\006\163\346\330\104\060\171\120\127\277") +var encoderTests = [][]byte{ + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015\016"), + []byte("\001\002\377"), + []byte("\001\002"), + []byte("\001\002\377\377"), + []byte("\001\002\003\004\005"), + []byte("\001\002\003\004\005\006"), + []byte("\001\002\003\004\005\006\007"), + []byte("\001\002\003\004\005\006\007\010"), + []byte("\001\002\003\004\005\006\007\010\011"), + []byte("\001\002\003\004\005\006\007\010\011\012"), + []byte("\001\002\003\004\005\006\007\010\011\012\013"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020"), + []byte("\000\000\000\000\377\377\377\377\125\125\125\125\252\252\252\252" + + "\201\143\310\322\307\174\262\027\137\117\316\311\111\055\122\041" + + "\141\251\161\040\045\263\006\163\346\330\104\060\171\120\127\277"), + []byte(""), + []byte("\001"), +} func Test_Base32Encoder(t *testing.T) { - encoder := Base32Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, "=") - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base32Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, "=") + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base64_test.go b/internal/util/enc/base64_test.go index 2baa0ee..d65448b 100644 --- a/internal/util/enc/base64_test.go +++ b/internal/util/enc/base64_test.go @@ -6,11 +6,13 @@ import ( ) func Test_Base64Encoder(t *testing.T) { - encoder := Base64Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, "=") - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base64Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, "=") + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base64u_test.go b/internal/util/enc/base64u_test.go index b5658a7..bb87b41 100644 --- a/internal/util/enc/base64u_test.go +++ b/internal/util/enc/base64u_test.go @@ -6,11 +6,13 @@ import ( ) func Test_Base64uEncoder(t *testing.T) { - encoder := Base64uEncoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, "=") - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base64uEncoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, "=") + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base85.go b/internal/util/enc/base85.go index 0f84b6e..158dbc4 100644 --- a/internal/util/enc/base85.go +++ b/internal/util/enc/base85.go @@ -6,9 +6,9 @@ import ( "github.com/pkg/errors" ) -// ------------------------------------------------------- +// This is a modifid version of Base85 which uses an alternate alphabet -- it skips the '.' (dot) and '`' (backtick) +// to make it more compatible with DNS domain-name system. -// Base64Encoder encodes 3 bytes to 4 characters type Base85Encoder struct { } @@ -65,7 +65,7 @@ func (b *Base85Encoder) Decode(data []byte) ([]byte, error) { func (b *Base85Encoder) TestPatterns() [][]byte { str := make([]byte, 85) // 33 (!) through 117 (u) - for k, _ := range str { + for k := range str { b := byte(k + 33) if b == '.' { str[k] = 'v' diff --git a/internal/util/enc/base85_test.go b/internal/util/enc/base85_test.go index dccb7ab..ec2c3d4 100644 --- a/internal/util/enc/base85_test.go +++ b/internal/util/enc/base85_test.go @@ -6,10 +6,12 @@ import ( ) func Test_Base85Encoder(t *testing.T) { - encoder := Base85Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base85Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base91_test.go b/internal/util/enc/base91_test.go index 8365b7c..40d481f 100644 --- a/internal/util/enc/base91_test.go +++ b/internal/util/enc/base91_test.go @@ -6,10 +6,12 @@ import ( ) func Test_Base91Encoder(t *testing.T) { - encoder := Base91Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base91Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base_192.go b/internal/util/enc/base_192.go deleted file mode 100644 index 8166b6d..0000000 --- a/internal/util/enc/base_192.go +++ /dev/null @@ -1,6 +0,0 @@ -package enc - -// TODO: Base 192 would be even more optimal than Base 128. -// Base192 could take the top 192 characters (leaving out the bottom 32, which are usually control characters). -// Base192 encoding could achieve near-raw efficiency, as it encodes 7.5 bits / byte. Or, in other words: every -// 15 bytes get encoded into 16 octets. This yields an appropriate 6.66% encoding loss. diff --git a/internal/util/enc/interface.go b/internal/util/enc/interface.go index 5667a58..a3ac5aa 100644 --- a/internal/util/enc/interface.go +++ b/internal/util/enc/interface.go @@ -33,6 +33,7 @@ var ( Base85Encoding Encoder = &Base85Encoder{} Base91Encoding Encoder = &Base91Encoder{} Base128Encoding Encoder = &Base128Encoder{} + Base192Encoding Encoder = &Base192Encoder{} RawEncoding Encoder = &RawEncoder{} ) @@ -46,6 +47,7 @@ func FromCode(code byte) (Encoder, error) { Base85Encoding, Base91Encoding, Base128Encoding, + Base192Encoding, RawEncoding, } { if enc.Code() == code { diff --git a/internal/util/mime/fieldsplitter.go b/internal/util/mime/fieldsplitter.go index 23ee8d7..2fdad8d 100644 --- a/internal/util/mime/fieldsplitter.go +++ b/internal/util/mime/fieldsplitter.go @@ -4,7 +4,7 @@ import "regexp" var commaSeparator = regexp.MustCompile("\\s*,\\s*") -// SplitField will take a comma-separated list and return the values (without potential blanks inbetween) +// SplitField will take a comma-separated list and return the values (without potential blanks in between) func SplitField(s string) []string { return commaSeparator.Split(s, -1) }