Implement basic TCP proxy
This commit is contained in:
parent
21eed45822
commit
c0f5ca6b39
44
config.go
44
config.go
|
@ -1,19 +1,52 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"io"
|
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/google/shlex"
|
"github.com/google/shlex"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Directive struct {
|
type Directive struct {
|
||||||
|
Name string
|
||||||
Params []string
|
Params []string
|
||||||
Children []*Directive
|
Children []*Directive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Directive) ParseParams(params ...*string) error {
|
||||||
|
if len(d.Params) < len(params) {
|
||||||
|
return fmt.Errorf("directive %q: want %v params, got %v", d.Name, len(params), len(d.Params))
|
||||||
|
}
|
||||||
|
for i, ptr := range params {
|
||||||
|
if ptr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
*ptr = d.Params[i]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Directive) ChildrenByName(name string) []*Directive {
|
||||||
|
l := make([]*Directive, 0, len(d.Children))
|
||||||
|
for _, child := range d.Children {
|
||||||
|
if child.Name == name {
|
||||||
|
l = append(l, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Directive) ChildByName(name string) *Directive {
|
||||||
|
for _, child := range d.Children {
|
||||||
|
if child.Name == name {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func Load(path string) ([]*Directive, error) {
|
func Load(path string) ([]*Directive, error) {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -48,13 +81,12 @@ func Parse(r io.Reader) ([]*Directive, error) {
|
||||||
|
|
||||||
var d *Directive
|
var d *Directive
|
||||||
if words[len(words)-1] == "{" && l[len(l)-1] == '{' {
|
if words[len(words)-1] == "{" && l[len(l)-1] == '{' {
|
||||||
d = &Directive{
|
words = words[:len(words)-1]
|
||||||
Params: words[:len(words) - 1],
|
d = &Directive{Params: words}
|
||||||
}
|
|
||||||
cur = d
|
cur = d
|
||||||
directives = append(directives, d)
|
directives = append(directives, d)
|
||||||
} else {
|
} else {
|
||||||
d = &Directive{Params: words}
|
d = &Directive{Name: words[0], Params: words[1:]}
|
||||||
if cur != nil {
|
if cur != nil {
|
||||||
cur.Children = append(cur.Children, d)
|
cur.Children = append(cur.Children, d)
|
||||||
} else {
|
} else {
|
||||||
|
|
90
main.go
90
main.go
|
@ -1,7 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -9,5 +13,89 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to load config file: %v", err)
|
log.Fatalf("failed to load config file: %v", err)
|
||||||
}
|
}
|
||||||
_ = directives
|
|
||||||
|
srv := &Server{}
|
||||||
|
|
||||||
|
for _, d := range directives {
|
||||||
|
if err := parseFrontend(srv, d); err != nil {
|
||||||
|
log.Fatalf("failed to parse frontend: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFrontend(srv *Server, d *Directive) error {
|
||||||
|
frontend := &Frontend{Server: srv}
|
||||||
|
srv.Frontends = append(srv.Frontends, frontend)
|
||||||
|
|
||||||
|
// TODO: support multiple backends
|
||||||
|
backendDirective := d.ChildByName("backend")
|
||||||
|
if backendDirective == nil {
|
||||||
|
return fmt.Errorf("missing backend directive in frontend block")
|
||||||
|
}
|
||||||
|
if err := parseBackend(&frontend.Backend, backendDirective); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, listenDirective := range d.ChildrenByName("listen") {
|
||||||
|
var listenAddr string
|
||||||
|
if err := listenDirective.ParseParams(&listenAddr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(listenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse listen address %q: %v", listenAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: come up with something more robust
|
||||||
|
if host != "localhost" && net.ParseIP(host) == nil {
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", net.JoinHostPort(host, port))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to listen on %q: %v", listenAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := frontend.Serve(ln); err != nil {
|
||||||
|
log.Fatalf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBackend(backend *Backend, d *Directive) error {
|
||||||
|
var backendURI string
|
||||||
|
if err := d.ParseParams(&backendURI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !strings.Contains(backendURI, ":/") {
|
||||||
|
// This is a raw domain name, make it an URL with an empty scheme
|
||||||
|
backendURI = "//" + backendURI
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(backendURI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse backend URI %q: %v", backendURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: +proxy to use the PROXY protocol
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "", "tcp":
|
||||||
|
backend.Network = "tcp"
|
||||||
|
backend.Address = u.Host
|
||||||
|
case "unix":
|
||||||
|
backend.Network = "unix"
|
||||||
|
backend.Address = u.Host
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("failed to setup backend %q: unsupported URI scheme", backendURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Frontends []*Frontend
|
||||||
|
}
|
||||||
|
|
||||||
|
type Frontend struct {
|
||||||
|
Server *Server
|
||||||
|
Backend Backend
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fe *Frontend) Serve(ln net.Listener) error {
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to accept connection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: log errors to debug log
|
||||||
|
go fe.handle(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fe *Frontend) handle(downstream net.Conn) error {
|
||||||
|
defer downstream.Close()
|
||||||
|
|
||||||
|
be := &fe.Backend
|
||||||
|
upstream, err := net.Dial(be.Network, be.Address)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to dial backend: %v", err)
|
||||||
|
}
|
||||||
|
defer upstream.Close()
|
||||||
|
|
||||||
|
return duplexCopy(upstream, downstream)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Backend struct {
|
||||||
|
Network string
|
||||||
|
Address string
|
||||||
|
}
|
||||||
|
|
||||||
|
func duplexCopy(a, b io.ReadWriter) error {
|
||||||
|
done := make(chan error, 2)
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(a, b)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(b, a)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
return <-done
|
||||||
|
}
|
Loading…
Reference in New Issue