This commit is contained in:
Jonathan Leibiusky @xetorthio
2017-06-19 11:03:31 -03:00
parent 1ee90c43d1
commit cd6d172cfa
6 changed files with 173 additions and 33 deletions

111
router/dns/dns.go Normal file
View File

@@ -0,0 +1,111 @@
package dns
import (
"fmt"
"log"
"net"
"strings"
"github.com/miekg/dns"
"github.com/play-with-docker/play-with-docker/config"
)
func DnsRequest(w dns.ResponseWriter, r *dns.Msg) {
if len(r.Question) > 0 && config.NameFilter.MatchString(r.Question[0].Name) {
// this is something we know about and we should try to handle
question := r.Question[0].Name
match := config.NameFilter.FindStringSubmatch(question)
ip := strings.Replace(match[1], "-", ".", -1)
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
m.RecursionAvailable = true
a, err := dns.NewRR(fmt.Sprintf("%s 60 IN A %s", question, ip))
if err != nil {
log.Fatal(err)
}
m.Answer = append(m.Answer, a)
w.WriteMsg(m)
return
} else if len(r.Question) > 0 && config.AliasFilter.MatchString(r.Question[0].Name) {
// this is something we know about and we should try to handle
question := r.Question[0].Name
match := config.AliasFilter.FindStringSubmatch(question)
i := core.InstanceFindByAlias(match[2], match[1])
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
m.RecursionAvailable = true
a, err := dns.NewRR(fmt.Sprintf("%s 60 IN A %s", question, i.IP))
if err != nil {
log.Fatal(err)
}
m.Answer = append(m.Answer, a)
w.WriteMsg(m)
return
} else {
if len(r.Question) > 0 {
question := r.Question[0].Name
if question == "localhost." {
log.Printf("Not a PWD host. Asked for [localhost.] returning automatically [127.0.0.1]\n")
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
m.RecursionAvailable = true
a, err := dns.NewRR(fmt.Sprintf("%s 60 IN A 127.0.0.1", question))
if err != nil {
log.Fatal(err)
}
m.Answer = append(m.Answer, a)
w.WriteMsg(m)
return
}
log.Printf("Not a PWD host. Looking up [%s]\n", question)
ips, err := net.LookupIP(question)
if err != nil {
// we have no information about this and we are not a recursive dns server, so we just fail so the client can fallback to the next dns server it has configured
w.Close()
// dns.HandleFailed(w, r)
return
}
log.Printf("Not a PWD host. Looking up [%s] got [%s]\n", question, ips)
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
m.RecursionAvailable = true
for _, ip := range ips {
ipv4 := ip.To4()
if ipv4 == nil {
a, err := dns.NewRR(fmt.Sprintf("%s 60 IN AAAA %s", question, ip.String()))
if err != nil {
log.Fatal(err)
}
m.Answer = append(m.Answer, a)
} else {
a, err := dns.NewRR(fmt.Sprintf("%s 60 IN A %s", question, ipv4.String()))
if err != nil {
log.Fatal(err)
}
m.Answer = append(m.Answer, a)
}
}
w.WriteMsg(m)
return
} else {
log.Printf("Not a PWD host. Got DNS without any question\n")
// we have no information about this and we are not a recursive dns server, so we just fail so the client can fallback to the next dns server it has configured
w.Close()
// dns.HandleFailed(w, r)
return
}
}
}

117
router/router.go Normal file
View File

@@ -0,0 +1,117 @@
package router
import (
"fmt"
"io"
"log"
"net"
vhost "github.com/inconshreveable/go-vhost"
)
type Director func(host string) (*net.TCPAddr, error)
type proxyRouter struct {
director Director
}
func (r *proxyRouter) Listen(laddr string) {
l, err := net.Listen("tcp", laddr)
defer l.Close()
if err != nil {
log.Fatal(err)
}
for {
conn, err := l.Accept()
if err != nil {
log.Println(err)
continue
}
go r.handleConnection(conn)
}
}
func (r *proxyRouter) handleConnection(c net.Conn) {
defer c.Close()
// first try tls
vhostConn, err := vhost.TLS(c)
if err != nil {
log.Printf("Incoming TLS connection produced an error. Error: %s", err)
return
}
defer vhostConn.Close()
host := vhostConn.ClientHelloMsg.ServerName
c.LocalAddr()
dstHost, err := r.director(fmt.Sprintf("%s:%d", host, 12))
if err != nil {
log.Printf("Error directing request: %v\n", err)
return
}
d, err := net.Dial("tcp", dstHost.String())
if err != nil {
log.Printf("Error dialing backend %s: %v\n", dstHost.String(), err)
return
}
errc := make(chan error, 2)
cp := func(dst io.Writer, src io.Reader) {
_, err := io.Copy(dst, src)
errc <- err
}
go cp(d, vhostConn)
go cp(vhostConn, d)
<-errc
/*
req, err := http.ReadRequest(bufio.NewReader(c))
if err != nil {
log.Println(err)
return
}
log.Println(req.Header)
*/
}
func NewRouter(director Director) *proxyRouter {
return &proxyRouter{director: director}
}
/*
// Start the DNS server
dns.HandleFunc(".", routerDns.DnsRequest)
udpDnsServer := &dns.Server{Addr: ":53", Net: "udp"}
go func() {
err := udpDnsServer.ListenAndServe()
if err != nil {
log.Fatal(err)
}
}()
tcpDnsServer := &dns.Server{Addr: ":53", Net: "tcp"}
go func() {
err := tcpDnsServer.ListenAndServe()
if err != nil {
log.Fatal(err)
}
}()
r := mux.NewRouter()
tcpHandler := handlers.NewTCPProxy()
r.Host(fmt.Sprintf("{subdomain:.*}pwd{node:%s}-{port:%s}.{tld:.*}", config.PWDHostnameRegex, config.PortRegex)).Handler(tcpHandler)
r.Host(fmt.Sprintf("{subdomain:.*}pwd{node:%s}.{tld:.*}", config.PWDHostnameRegex)).Handler(tcpHandler)
r.Host(fmt.Sprintf("pwd{alias:%s}-{session:%s}-{port:%s}.{tld:.*}", config.AliasnameRegex, config.AliasSessionRegex, config.PortRegex)).Handler(tcpHandler)
r.Host(fmt.Sprintf("pwd{alias:%s}-{session:%s}.{tld:.*}", config.AliasnameRegex, config.AliasSessionRegex)).Handler(tcpHandler)
r.HandleFunc("/ping", handlers.Ping).Methods("GET")
n := negroni.Classic()
n.UseHandler(r)
httpServer := http.Server{
Addr: "0.0.0.0:" + config.PortNumber,
Handler: n,
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
}
// Now listen for TLS connections that need to be proxied
tls.StartTLSProxy(config.SSLPortNumber)
http.ListenAndServe()
*/

49
router/router_test.go Normal file
View File

@@ -0,0 +1,49 @@
package router
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestProxy_TLS(t *testing.T) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
const msg = "It works!"
var receivedHost string
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, msg)
}))
defer ts.Close()
r := NewRouter(func(host string) (*net.TCPAddr, error) {
receivedHost = host
u, _ := url.Parse(ts.URL)
a, _ := net.ResolveTCPAddr("tcp", u.Host)
return a, nil
})
go r.Listen(":8080")
req, err := http.NewRequest("GET", "https://localhost:8080", nil)
assert.Nil(t, err)
resp, err := client.Do(req)
assert.Nil(t, err)
body, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
assert.Equal(t, msg, string(body))
assert.Equal(t, "localhost:8080", receivedHost)
}

143
router/tcp/tcp.go Normal file
View File

@@ -0,0 +1,143 @@
package tcp
import (
"crypto/tls"
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
"github.com/franela/pwd.old/config"
"github.com/gorilla/mux"
)
func getTargetInfo(vars map[string]string, req *http.Request) (string, string) {
node := vars["node"]
port := vars["port"]
alias := vars["alias"]
sessionPrefix := vars["session"]
hostPort := strings.Split(req.Host, ":")
// give priority to the URL host port
if len(hostPort) > 1 && hostPort[1] != config.PortNumber {
port = hostPort[1]
} else if port == "" {
port = "80"
}
if alias != "" {
instance := core.InstanceFindByAlias(sessionPrefix, alias)
if instance != nil {
node = instance.IP
return node, port
}
}
// Node is actually an ip, need to convert underscores by dots.
ip := strings.Replace(node, "-", ".", -1)
if net.ParseIP(ip) == nil {
// Not a valid IP, so treat this is a hostname.
} else {
node = ip
}
return node, port
}
type tcpProxy struct {
Director func(*http.Request)
ErrorLog *log.Logger
Dial func(network, addr string) (net.Conn, error)
}
func (p *tcpProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logFunc := log.Printf
if p.ErrorLog != nil {
logFunc = p.ErrorLog.Printf
}
vars := mux.Vars(r)
instanceIP := vars["node"]
if i := core.InstanceFindByIP(strings.Replace(instanceIP, "-", ".", -1)); i == nil {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
outreq := new(http.Request)
// shallow copying
*outreq = *r
p.Director(outreq)
host := outreq.URL.Host
dial := p.Dial
if dial == nil {
dial = net.Dial
}
if outreq.URL.Scheme == "wss" || outreq.URL.Scheme == "https" {
var tlsConfig *tls.Config
tlsConfig = &tls.Config{InsecureSkipVerify: true}
dial = func(network, address string) (net.Conn, error) {
return tls.Dial("tcp", host, tlsConfig)
}
}
d, err := dial("tcp", host)
if err != nil {
http.Error(w, "Error forwarding request.", 500)
logFunc("Error dialing websocket backend %s: %v", outreq.URL, err)
return
}
// All request generated by the http package implement this interface.
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Not a hijacker?", 500)
return
}
// Hijack() tells the http package not to do anything else with the connection.
// After, it bcomes this functions job to manage it. `nc` is of type *net.Conn.
nc, _, err := hj.Hijack()
if err != nil {
logFunc("Hijack error: %v", err)
return
}
defer nc.Close() // must close the underlying net connection after hijacking
defer d.Close()
// write the modified incoming request to the dialed connection
err = outreq.Write(d)
if err != nil {
logFunc("Error copying request to target: %v", err)
return
}
errc := make(chan error, 2)
cp := func(dst io.Writer, src io.Reader) {
_, err := io.Copy(dst, src)
errc <- err
}
go cp(d, nc)
go cp(nc, d)
<-errc
}
func NewTCPProxy() http.Handler {
director := func(req *http.Request) {
v := mux.Vars(req)
node, port := getTargetInfo(v, req)
if port == "443" {
if strings.Contains(req.URL.Scheme, "http") {
req.URL.Scheme = "https"
} else {
req.URL.Scheme = "wss"
}
}
req.URL.Host = fmt.Sprintf("%s:%s", node, port)
}
return &tcpProxy{Director: director}
}

95
router/tls/tls.go Normal file
View File

@@ -0,0 +1,95 @@
package tls
import (
"fmt"
"io"
"log"
"net"
"strings"
vhost "github.com/inconshreveable/go-vhost"
"github.com/play-with-docker/play-with-docker/config"
)
func StartTLSProxy(port string) {
tlsListener, tlsErr := net.Listen("tcp", fmt.Sprintf(":%s", port))
log.Println("Listening on port " + port)
if tlsErr != nil {
log.Fatal(tlsErr)
}
defer tlsListener.Close()
for {
// Wait for TLS Connection
conn, err := tlsListener.Accept()
if err != nil {
log.Printf("Could not accept new TLS connection. Error: %s", err)
continue
}
// Handle connection on a new goroutine and continue accepting other new connections
go func(c net.Conn) {
defer c.Close()
vhostConn, err := vhost.TLS(conn)
if err != nil {
log.Printf("Incoming TLS connection produced an error. Error: %s", err)
return
}
defer vhostConn.Close()
var targetIP string
targetPort := "443"
host := vhostConn.ClientHelloMsg.ServerName
match := config.NameFilter.FindStringSubmatch(host)
if len(match) < 2 {
// Not a valid proxy host, try alias hosts
match := config.AliasFilter.FindStringSubmatch(host)
if len(match) < 4 {
// Not valid, just close the connection
return
} else {
alias := match[1]
sessionPrefix := match[2]
instance := core.InstanceFindByAlias(sessionPrefix, alias)
if instance != nil {
targetIP = instance.IP
} else {
return
}
if len(match) == 4 {
targetPort = match[3]
}
}
} else {
// Valid proxy host
ip := strings.Replace(match[1], "-", ".", -1)
if net.ParseIP(ip) == nil {
// Not a valid IP, so treat this is a hostname.
return
} else {
targetIP = ip
}
if len(match) == 3 {
targetPort = match[2]
}
}
dest := fmt.Sprintf("%s:%s", targetIP, targetPort)
d, err := net.Dial("tcp", dest)
if err != nil {
log.Printf("Error dialing backend %s: %v\n", dest, err)
return
}
errc := make(chan error, 2)
cp := func(dst io.Writer, src io.Reader) {
_, err := io.Copy(dst, src)
errc <- err
}
go cp(d, vhostConn)
go cp(vhostConn, d)
<-errc
}(conn)
}
}