├── .gitignore ├── README.markdown ├── api.go ├── dist ├── dns.go ├── install.go ├── main.go ├── main_test.go ├── pharod-start ├── pharod-stop └── pharodctl └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /pharod 2 | /pharodctl/pharodctl 3 | /build/ 4 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Pharod 2 | 3 | The problem: you have all kinds of things running in Docker containers on your Mac, and to get at them you have to remember which darned ports you got them listening on. Binding to well-known ports works great until you need to work on two projects at once. You wish you could just address your containers with nice hostnames, on the port that is natural for the service in question. 4 | 5 | The solution: a daemon that monitors Docker to watch for containers with exposed ports, works out the port number that you want to use, and listens on that port on the next available 127.0.0.0/8 alias (the whole block points back to the loopback device, not just 127.0.0.1). It can then resolve a hostname derived from the container name to that address. The effect: 6 | 7 | ``` 8 | $ docker run -dP --name redis redis 9 | b58899b0319ca62f2fe5d97e1ea34204f73ce1b419c56c7e29aab8efac227036 10 | $ pharodctl ls 11 | redis.pharod: 127.2.2.1:6379 -> 192.168.64.3:32769 12 | 13 | # wait a few seconds for Redis to start... 14 | 15 | $ redis-cli -h redis.pharod 16 | redis.pharod:6379> set hello "there" 17 | OK 18 | ``` 19 | 20 | But we can also run another Redis, and we still don't have to worry about ports: 21 | 22 | ``` 23 | $ docker run -dP --name redis2 redis 24 | 75bc2222924d8321633dd1a974d5b72ea7c8b31e6c59a64f0cacbe06463de76a 25 | $ pharodctl ls 26 | redis.pharod: 127.2.2.1:6379 -> 192.168.64.3:32769 27 | redis2.pharod: 127.2.2.2:6379 -> 192.168.64.3:32770 28 | $ redis-cli -h redis2.pharod 29 | redis2.pharod:6379> get hello 30 | (nil) 31 | ``` 32 | 33 | Note the second forwarder uses a new loopback IP address. Pharod will only use new addresses when there would be a clash; it uses the same one for many if forwarders can exist on different ports. 34 | 35 | At the moment Pharod is built for OS X, particularly in the hostname resolving, but I don't think there's any reason in principle why it wouldn't work on Linux too, as long as you could find some way to hook into system-wide hostname resolving on a per-TLD basis. OS X makes that easy: we just make a config file in `/etc/resolver/pharod`. 36 | 37 | ## Using 38 | 39 | It's currently in our Homebrew repository, so: 40 | 41 | ``` 42 | $ brew tap madebymany/custom 43 | $ brew install pharod 44 | ``` 45 | 46 | Then use `pharod-start` to start it, and `pharodctl ls` to inspect its state. It requires using [dlite](https://github.com/nlf/dlite) or the Docker for Mac beta. Stop it with `pharod-stop`. It doesn't use launchd because it's quite nice to be able to inspect possible errors on startup like this. Might integrate when it's more mature. 47 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | const APIUnixSocket = "/tmp/pharod.sock" 12 | 13 | func handleAPIClient(c net.Conn) { 14 | cReader := bufio.NewReader(c) 15 | defer c.Close() 16 | cmd, err := cReader.ReadString('\n') 17 | if cmd == "" && err != nil { 18 | return 19 | } 20 | 21 | cmd = strings.TrimSpace(cmd) 22 | switch cmd { 23 | case "ls", "listListeners": 24 | for _, l := range containerListeners { 25 | fmt.Fprintf(c, "%s.%s: %s:%d -> %s:%d\n", l.DNSName, DnsTld, 26 | l.Src.IP, l.Src.Port, l.Dest.IP, l.Dest.Port) 27 | } 28 | } 29 | } 30 | 31 | func startAPI() { 32 | if _, err := os.Stat(APIUnixSocket); err == nil { 33 | err = os.Remove(APIUnixSocket) 34 | if err != nil { 35 | panic(err) 36 | } 37 | } 38 | 39 | server, err := net.Listen("unix", APIUnixSocket) 40 | if err != nil { 41 | panic(err) 42 | return 43 | } 44 | 45 | err = os.Chmod(APIUnixSocket, os.FileMode(0777)) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | for { 51 | conn, err := server.Accept() 52 | if err != nil { 53 | return 54 | } 55 | 56 | go handleAPIClient(conn) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /dist: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BUILD_DIR=build 4 | DIST_SCRIPTS="pharod-start pharod-stop" 5 | 6 | set -eo pipefail 7 | 8 | name=pharod 9 | tarball=$name-dist.tar.gz 10 | 11 | cd "$(dirname "$0")" 12 | mkdir -p "$BUILD_DIR" 13 | 14 | version="$(git log -1 --format="%ct")" 15 | echo Building... 16 | go build 17 | mv pharod "${BUILD_DIR}/" 18 | cd pharodctl 19 | go build 20 | mv pharodctl "../${BUILD_DIR}/" 21 | cd .. 22 | cp $DIST_SCRIPTS "${BUILD_DIR}/" 23 | 24 | cd "$BUILD_DIR" 25 | [[ -f $tarball ]] && rm "$tarball" 26 | tar -czf "$tarball" pharod pharodctl $DIST_SCRIPTS 27 | 28 | destination="s3://mxm-golang-binaries/${name}/${name}-${version}.tar.gz" 29 | echo Uploading to "${destination}" 30 | AWS_DEFAULT_PROFILE=MxM aws s3 cp --acl public-read "$tarball" "$destination" 31 | echo -e "\nUpdate https://github.com/madebymany/homebrew-custom/blob/master/pharod.rb with:\n version '${version}'" 32 | echo " sha256 '$(shasum -a 256 "$tarball" | cut -f1 -d' ')'" 33 | cd .. 34 | rm -r "$BUILD_DIR" 35 | -------------------------------------------------------------------------------- /dns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/miekg/dns" 6 | "strings" 7 | ) 8 | 9 | func startDns() { 10 | dnsSuffix := "." + DnsTld + "." 11 | parseDnsName := func(name string) (out string, err error) { 12 | if strings.HasSuffix(name, dnsSuffix) { 13 | return strings.ToLower(strings.TrimSuffix(name, dnsSuffix)), nil 14 | } else { 15 | return "", fmt.Errorf("nxdomain: %s", name) 16 | } 17 | } 18 | 19 | s := dns.Server{ 20 | Addr: "127.0.0.1:49152", 21 | Net: "udp", 22 | Handler: dns.HandlerFunc(func(w dns.ResponseWriter, qMsg *dns.Msg) { 23 | rMsg := new(dns.Msg) 24 | if len(qMsg.Question) == 0 { 25 | rMsg.SetRcode(qMsg, dns.RcodeServerFailure) 26 | } else { 27 | query := qMsg.Question[0] 28 | qname, err := parseDnsName(query.Name) 29 | 30 | if ip, ok := dnsZone[qname]; err == nil && query.Qtype == dns.TypeA && ok { 31 | rMsg.SetReply(qMsg) 32 | rr := new(dns.A) 33 | rr.Hdr = dns.RR_Header{Name: query.Name, Rrtype: dns.TypeA, 34 | Class: dns.ClassINET, Ttl: 0} 35 | rr.A = ip 36 | rMsg.Answer = append(rMsg.Answer, rr) 37 | } else { 38 | rMsg.SetRcode(qMsg, dns.RcodeNameError) 39 | } 40 | } 41 | 42 | w.WriteMsg(rMsg) 43 | }), 44 | } 45 | s.ListenAndServe() 46 | } 47 | -------------------------------------------------------------------------------- /install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "regexp" 11 | ) 12 | 13 | const resolverConfigStr = `# Carefully generated by pharod 14 | nameserver 127.0.0.1 15 | port 49152 16 | ` 17 | const resolverConfigPath = "/etc/resolver/pharod" 18 | 19 | const resetLocationScript = `# Borrowed from pow's install.sh 20 | set -e 21 | location=$(networksetup -getcurrentlocation) 22 | templocation="pharod$$" 23 | networksetup -createlocation "$templocation" >/dev/null 2>&1 24 | networksetup -switchtolocation "$templocation" >/dev/null 2>&1 25 | networksetup -switchtolocation "$location" >/dev/null 2>&1 26 | networksetup -deletelocation "$templocation" >/dev/null 2>&1 27 | ` 28 | const newPowRuleFormat = `table { 0/0, 127.0.0.1, !127/8 } 29 | rdr pass proto tcp from any to port {80,%[1]s} -> 127.0.0.1 port %[1]s 30 | ` 31 | 32 | var powRulePattern = regexp.MustCompile(`(?m)^rdr pass inet proto tcp from any to any port = (\d+)`) 33 | 34 | func install() (err error) { 35 | resolverConfig := []byte(resolverConfigStr) 36 | 37 | currentConfig, err := ioutil.ReadFile(resolverConfigPath) 38 | if err != nil || sha256.Sum256(currentConfig) != sha256.Sum256(resolverConfig) { 39 | log.Println("Resolver config needs updating; writing new one now") 40 | err = ioutil.WriteFile(resolverConfigPath, resolverConfig, 0644) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | log.Println("Prodding OS X to make it aware of new resolver settings") 46 | err = runCommandOutStdout(exec.Command( 47 | "sh", "-c", resetLocationScript)) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | } 53 | 54 | // Reconfigure Pow rules if present 55 | powRules, err := exec.Command("pfctl", "-a", "com.apple/250.PowFirewall", "-s", "nat").Output() 56 | if err != nil { 57 | return pfctlError(err) 58 | } 59 | 60 | if m := powRulePattern.FindAllStringSubmatch(string(powRules), 2); len(m) > 0 { 61 | log.Println("Reconfiguring Pow firewall to allow Pharod listeners on port 80") 62 | powPort := "" 63 | for _, ml := range m { 64 | if ml[1] != "80" { 65 | powPort = ml[1] 66 | break 67 | } 68 | } 69 | if powPort == "" { 70 | return fmt.Errorf("unable to find Pow port; it's probably changed. Report a pharod bug please!") 71 | } 72 | 73 | newPowRules := fmt.Sprintf(newPowRuleFormat, powPort) 74 | 75 | pfctl := exec.Command("pfctl", "-a", "com.apple/250.PowFirewall", 76 | "-Ef", "-") 77 | pfctlIn, err := pfctl.StdinPipe() 78 | if err != nil { 79 | return pfctlError(err) 80 | } 81 | 82 | err = pfctl.Start() 83 | if err != nil { 84 | return pfctlError(err) 85 | } 86 | 87 | _, err = pfctlIn.Write([]byte(newPowRules)) 88 | if err != nil { 89 | return pfctlError(err) 90 | } 91 | 92 | err = pfctlIn.Close() 93 | if err != nil { 94 | return pfctlError(err) 95 | } 96 | 97 | err = pfctl.Wait() 98 | if err != nil { 99 | return pfctlError(err) 100 | } 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func runCommandOutStdout(cmd *exec.Cmd) error { 107 | cmd.Stdout = os.Stdout 108 | cmd.Stderr = os.Stderr 109 | return cmd.Run() 110 | } 111 | 112 | func pfctlError(errIn error) error { 113 | return fmt.Errorf("pfctl: %s", errIn) 114 | } 115 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/fsouza/go-dockerclient" 7 | "github.com/sevlyar/go-daemon" 8 | "io" 9 | "log" 10 | "net" 11 | "net/url" 12 | "os" 13 | "os/exec" 14 | "os/user" 15 | "path" 16 | "regexp" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | ) 21 | 22 | var sourceAddrs map[string]map[int]*net.TCPAddr 23 | var dnsZone map[string]net.IP 24 | var containerListeners map[string]*Listener 25 | var dockerIP net.IP 26 | var SourceStartIP = net.ParseIP("127.2.2.1") 27 | var firstEphemeralPort int 28 | 29 | const DnsTld = "pharod" 30 | 31 | var shouldDaemonize = flag.Bool("d", false, "run in background") 32 | 33 | type Listener struct { 34 | DNSName string 35 | Src *net.TCPAddr 36 | Dest *net.TCPAddr 37 | shouldStop bool 38 | finished *sync.WaitGroup 39 | tcpListener *net.TCPListener 40 | newConn chan net.Conn 41 | closedConn chan net.Conn 42 | closeAllConns chan struct{} 43 | } 44 | 45 | func newDockerClient(host string) (client *docker.Client, err error) { 46 | if os.Getenv("DOCKER_TLS_VERIFY") != "" { 47 | dockerCertPath := os.Getenv("DOCKER_CERT_PATH") 48 | if dockerCertPath == "" { 49 | return nil, fmt.Errorf("docker TLS required, but no DOCKER_CERT_PATH set") 50 | } 51 | 52 | return docker.NewTLSClient(host, 53 | path.Join(dockerCertPath, "cert.pem"), 54 | path.Join(dockerCertPath, "key.pem"), 55 | path.Join(dockerCertPath, "ca.pem")) 56 | } else { 57 | return docker.NewClient(host) 58 | } 59 | } 60 | 61 | func die(msg string) { 62 | fmt.Fprintln(os.Stderr, msg) 63 | os.Exit(1) 64 | } 65 | 66 | func containerPortKey(c *docker.Container, p docker.APIPort) string { 67 | return fmt.Sprintf("%s:%d", c.ID, p.PrivatePort) 68 | } 69 | 70 | func addContainer(dockerClient *docker.Client, cid string) (out []*Listener) { 71 | c, err := dockerClient.InspectContainer(cid) 72 | if err != nil { 73 | log.Printf("Getting container info failed for id %s: %s", cid, err) 74 | return nil 75 | } 76 | ports := c.NetworkSettings.PortMappingAPI() 77 | 78 | out = make([]*Listener, 0, len(ports)) 79 | for _, port := range ports { 80 | key := containerPortKey(c, port) 81 | if _, ok := containerListeners[key]; ok { 82 | // already started 83 | continue 84 | } 85 | l, err := ListenerFromContainerAndPort(c, port) 86 | if err != nil { 87 | log.Printf("Error creating listener for %v on container %s: %s", 88 | port, c.ID, err) 89 | continue 90 | } 91 | l.Start() 92 | containerListeners[key] = l 93 | dnsZone[l.DNSName] = l.Src.IP 94 | out = append(out, l) 95 | } 96 | return 97 | } 98 | 99 | func removeContainer(cid string) { 100 | for cp, l := range containerListeners { 101 | if strings.HasPrefix(cp, cid+":") { 102 | delete(containerListeners, cp) 103 | delete(dnsZone, l.DNSName) 104 | delete(sourceAddrs[l.Src.IP.String()], l.Src.Port) 105 | l.Stop() 106 | } 107 | } 108 | } 109 | 110 | func main() { 111 | var err error 112 | 113 | log.SetOutput(os.Stderr) 114 | flag.Parse() 115 | 116 | firstEphemeralPortStr := os.Getenv("DOCKER_FIRST_EPHEMERAL_PORT") 117 | if firstEphemeralPortStr == "" { 118 | firstEphemeralPortStr = "49152" 119 | } 120 | 121 | firstEphemeralPortInt64, err := strconv.ParseInt( 122 | firstEphemeralPortStr, 10, 64) 123 | if err != nil { 124 | die("error reading DOCKER_FIRST_EPHEMERAL_PORT: " + err.Error()) 125 | } 126 | firstEphemeralPort = int(firstEphemeralPortInt64) 127 | 128 | currentUser, err := user.Current() 129 | if err != nil { 130 | die(err.Error()) 131 | } 132 | if currentUser.Uid != "0" { 133 | die("Must be run as root") 134 | } 135 | 136 | if SourceStartIP == nil { 137 | panic("SourceStartIPStr not an IP address") 138 | } 139 | 140 | err = install() 141 | if err != nil { 142 | die(err.Error()) 143 | } 144 | 145 | dockerHost := os.Getenv("DOCKER_HOST") 146 | if dockerHost == "" { 147 | die("DOCKER_HOST not set") 148 | } 149 | 150 | dockerIpStr := os.Getenv("DOCKER_HOST_IP") 151 | if dockerIpStr == "" { 152 | dockerHostUrl, err := url.Parse(dockerHost) 153 | if err != nil { 154 | die(fmt.Sprintf("Couldn't parse DOCKER_HOST URL: %v", err)) 155 | } 156 | dockerIpStr, _, err = net.SplitHostPort(dockerHostUrl.Host) 157 | if err != nil { 158 | die(err.Error()) 159 | } 160 | } 161 | 162 | dockerIpAddr, err := net.ResolveIPAddr("ip", dockerIpStr) 163 | if err != nil { 164 | die(fmt.Sprintf("'%s' couldn't be resolved: %v", dockerIpStr, err)) 165 | } 166 | dockerIP = dockerIpAddr.IP 167 | 168 | if *shouldDaemonize { 169 | arg0 := os.Args[0] 170 | if arg0 == "" { 171 | panic("arg 0 is \"\"") 172 | } else if !strings.Contains(arg0, "/") { 173 | die("When daemonizing, pharod must be called with an absolute path, like /usr/bin/pharod") 174 | } 175 | 176 | dmn := &daemon.Context{ 177 | PidFileName: "/var/run/pharod.pid", 178 | PidFilePerm: 0644, 179 | LogFileName: "/var/log/pharod.log", 180 | LogFilePerm: 0640, 181 | WorkDir: "/", 182 | Umask: 027, 183 | } 184 | fmt.Println("Starting Pharod in the background...") 185 | child, err := dmn.Reborn() 186 | if err != nil { 187 | die(err.Error()) 188 | } 189 | if child != nil { 190 | fmt.Printf("Started as process %d. Check output in %s\n", 191 | child.Pid, dmn.LogFileName) 192 | os.Exit(0) 193 | } 194 | } 195 | 196 | log.Println("** Starting Pharod") 197 | 198 | dockerClient, err := newDockerClient(dockerHost) 199 | if err != nil { 200 | die(err.Error()) 201 | } 202 | 203 | dnsZone = make(map[string]net.IP, 0) 204 | containerListeners = make(map[string]*Listener) 205 | sourceAddrs = make(map[string]map[int]*net.TCPAddr) 206 | 207 | go startDns() 208 | go startAPI() 209 | 210 | dockerEvents := make(chan *docker.APIEvents) 211 | err = dockerClient.AddEventListener(dockerEvents) 212 | if err != nil { 213 | die(err.Error()) 214 | } 215 | 216 | containers, err := dockerClient.ListContainers(docker.ListContainersOptions{}) 217 | if err != nil { 218 | die(err.Error()) 219 | } 220 | 221 | for _, c := range containers { 222 | addContainer(dockerClient, c.ID) 223 | } 224 | 225 | for ev := range dockerEvents { 226 | switch ev.Status { 227 | case "start", "unpause": 228 | addContainer(dockerClient, ev.ID) 229 | case "stop", "pause", "die": 230 | removeContainer(ev.ID) 231 | } 232 | } 233 | } 234 | 235 | func succIP(ip net.IP) net.IP { 236 | if ip.To4() == nil { 237 | panic("only IPv4 supported at the moment") 238 | } 239 | ipInt := (uint32(ip[12]) << 24) | (uint32(ip[13]) << 16) | 240 | (uint32(ip[14]) << 8) | uint32(ip[15]) 241 | ipInt += 1 242 | return net.IP([]byte{ 243 | byte(ipInt >> 24), 244 | byte(ipInt >> 16), 245 | byte(ipInt >> 8), 246 | byte(ipInt), 247 | }) 248 | } 249 | 250 | func sourceAddrForPort(port int, dest *net.TCPAddr) *net.TCPAddr { 251 | getSourceAddr := func(addr string) *net.TCPAddr { 252 | src, err := net.ResolveTCPAddr("tcp", 253 | fmt.Sprintf("%s:%d", addr, port)) 254 | if err != nil { 255 | panic(err) 256 | } 257 | return src 258 | } 259 | 260 | var lastAddr string 261 | for addr, ls := range sourceAddrs { 262 | lastAddr = addr 263 | if _, ok := ls[port]; !ok { 264 | ls[port] = dest 265 | return getSourceAddr(addr) 266 | } 267 | } 268 | 269 | var nextIP net.IP 270 | if lastAddr == "" { 271 | nextIP = SourceStartIP 272 | } else { 273 | lastAddrIP := net.ParseIP(lastAddr) 274 | if lastAddrIP == nil { 275 | panic("lastAddr not an IP address") 276 | } 277 | nextIP = succIP(lastAddrIP) 278 | } 279 | 280 | if !nextIP.IsLoopback() { 281 | panic("ran out of loopback addresses!") 282 | } 283 | 284 | addr := nextIP.String() 285 | ifconfig := exec.Command("ifconfig", "lo0", "alias", addr, "up") 286 | if err := ifconfig.Run(); err != nil { 287 | panic(fmt.Sprintf( 288 | "error calling ifconfig, adding alias for %s: %s", 289 | addr, err)) 290 | } 291 | 292 | sourceAddrs[addr] = map[int]*net.TCPAddr{ 293 | port: dest, 294 | } 295 | return getSourceAddr(addr) 296 | } 297 | 298 | var dnsNameAllowedChars = regexp.MustCompile(`[^-a-z0-9]+`) 299 | var dnsNameHyphenStrings = regexp.MustCompile(`-{2,}`) 300 | 301 | func dnsNameFromContainerName(containerName string) string { 302 | return dnsNameHyphenStrings.ReplaceAllLiteralString( 303 | strings.Trim(dnsNameAllowedChars.ReplaceAllLiteralString( 304 | containerName, "-"), "-"), "-") 305 | } 306 | 307 | func ListenerFromContainerAndPort(container *docker.Container, port docker.APIPort) (out *Listener, err error) { 308 | 309 | if container.Name == "" { 310 | return nil, fmt.Errorf("Container %s has no name from which to build a DNS name", container.ID) 311 | } 312 | 313 | if port.PublicPort == 0 || port.PrivatePort == 0 { 314 | return nil, fmt.Errorf("Public port not exposed for %d on %s", 315 | port.PublicPort, container.Name) 316 | } 317 | 318 | out = &Listener{ 319 | finished: &sync.WaitGroup{}, 320 | newConn: make(chan net.Conn), 321 | closedConn: make(chan net.Conn), 322 | closeAllConns: make(chan struct{}), 323 | } 324 | 325 | out.DNSName = dnsNameFromContainerName(container.Name) 326 | if out.DNSName == "" { 327 | return nil, fmt.Errorf("Couldn't build a non-empty DNS name from '%s'", container.Name) 328 | } 329 | 330 | destIPAddr, err := net.ResolveIPAddr("ip", port.IP) 331 | if err != nil { 332 | return 333 | } 334 | out.Dest = new(net.TCPAddr) 335 | if destIPAddr.IP.IsUnspecified() { 336 | out.Dest.IP = dockerIP 337 | } else { 338 | out.Dest.IP = destIPAddr.IP 339 | } 340 | out.Dest.Port = int(port.PublicPort) 341 | out.Dest.Zone = destIPAddr.Zone 342 | 343 | var srcPort int 344 | /* If destination is an ephemeral port, we want to listen on the original 345 | * exposed port on the container, as that's the nice friendly one. If it's 346 | * not, we want to listen on the same port as we're forwarding to, as that 347 | * means the user has exposed a different port on the host. 348 | */ 349 | if out.Dest.Port >= firstEphemeralPort { 350 | srcPort = int(port.PrivatePort) 351 | } else { 352 | srcPort = out.Dest.Port 353 | } 354 | out.Src = sourceAddrForPort(srcPort, out.Dest) 355 | return 356 | } 357 | 358 | func (self *Listener) Start() { 359 | log.Printf("Started listener on %s; listening: %v; dialling: %v", self.DNSName, *self.Src, *self.Dest) 360 | var err error 361 | self.tcpListener, err = net.ListenTCP("tcp", self.Src) 362 | if err != nil { 363 | panic(err) 364 | } 365 | 366 | self.finished.Add(1) 367 | 368 | go func() { 369 | openConnections := make(map[net.Conn]bool) 370 | for { 371 | select { 372 | case conn := <-self.newConn: 373 | openConnections[conn] = true 374 | case conn := <-self.closedConn: 375 | delete(openConnections, conn) 376 | case _ = <-self.closeAllConns: 377 | for conn, _ := range openConnections { 378 | conn.Close() 379 | } 380 | self.finished.Done() 381 | return 382 | } 383 | } 384 | }() 385 | 386 | go func() { 387 | for { 388 | conn, err := self.tcpListener.Accept() 389 | if err != nil { 390 | log.Printf("Shutting down listener on %s", self.DNSName) 391 | self.closeAllConns <- struct{}{} 392 | return 393 | } 394 | 395 | self.newConn <- conn 396 | 397 | go self.forward(conn) 398 | } 399 | }() 400 | } 401 | 402 | func (self *Listener) Stop() { 403 | if self.tcpListener != nil { 404 | log.Printf("Stopping listener on %s", self.DNSName) 405 | self.tcpListener.Close() 406 | self.closeAllConns <- struct{}{} 407 | } 408 | } 409 | 410 | func (self *Listener) Wait() { 411 | self.finished.Wait() 412 | } 413 | 414 | func (self *Listener) forward(local net.Conn) { 415 | 416 | remote, err := net.DialTCP("tcp", nil, self.Dest) 417 | if err != nil { 418 | log.Printf("Remote dial failed: %v\n", err) 419 | return 420 | } 421 | 422 | wg := sync.WaitGroup{} 423 | wg.Add(2) 424 | self.finished.Add(2) 425 | 426 | go func() { 427 | defer local.Close() 428 | defer remote.Close() 429 | io.Copy(local, remote) 430 | self.finished.Done() 431 | wg.Done() 432 | }() 433 | 434 | go func() { 435 | defer local.Close() 436 | defer remote.Close() 437 | io.Copy(remote, local) 438 | self.finished.Done() 439 | wg.Done() 440 | }() 441 | 442 | wg.Wait() 443 | self.closedConn <- local 444 | } 445 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type stringFuncExpectation struct { 8 | in string 9 | expects string 10 | } 11 | 12 | func TestDnsNameFromContainerName(t *testing.T) { 13 | expectations := []stringFuncExpectation{ 14 | stringFuncExpectation{in: "pharod_db_1", expects: "pharod-db-1"}, 15 | stringFuncExpectation{in: "/pharod_db_1__", expects: "pharod-db-1"}, 16 | stringFuncExpectation{in: "/pharod_db__1.yes", expects: "pharod-db-1-yes"}, 17 | stringFuncExpectation{in: "ph!@£$arod_db_1", expects: "ph-arod-db-1"}, 18 | } 19 | 20 | for _, ex := range expectations { 21 | actual := dnsNameFromContainerName(ex.in) 22 | if actual != ex.expects { 23 | t.Logf("expected '%s', actual '%s'", ex.expects, actual) 24 | t.Fail() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pharod-start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | pharod="$(command -v pharod)" 5 | if [[ -z $pharod ]]; then 6 | echo "pharod not found in \$PATH, please check it's installed properly" 7 | exit 1 8 | fi 9 | 10 | if pgrep -qx pharod; then 11 | echo "You can only have one instance of pharod running at one time" 12 | exit 1 13 | fi 14 | 15 | sudo mkdir -p /etc/resolver 16 | 17 | if [[ $(docker version --format "{{.Server.KernelVersion}}") == *-moby ]]; then 18 | echo "** Detected Docker for Mac" 19 | 20 | DOCKER_HOST_IP="$HOSTNAME.local" 21 | # We have to hard-code this because we have no SSH access to the VM: 22 | DOCKER_FIRST_EPHEMERAL_PORT='32768' 23 | 24 | elif command -v dlite >/dev/null; then 25 | echo "** Docker for Mac not installed or not running; trying Dlite" 26 | 27 | if ! pgrep -qx dlite; then 28 | echo "dlite not running, please start it with 'dlite start'" 29 | exit 1 30 | fi 31 | 32 | echo "** Detected Dlite" 33 | 34 | DOCKER_HOST_IP="local.docker" 35 | 36 | if ! grep -qw "^$DOCKER_HOST_IP " ~/.ssh/known_hosts; then 37 | ssh-keyscan -t rsa "$DOCKER_HOST_IP" 2>/dev/null >>~/.ssh/known_hosts 38 | fi 39 | 40 | if [[ -z $DOCKER_FIRST_EPHEMERAL_PORT ]]; then 41 | # This is what Docker also reads the first port from. 42 | DOCKER_FIRST_EPHEMERAL_PORT="$(ssh docker@$DOCKER_HOST_IP cat /proc/sys/net/ipv4/ip_local_port_range | egrep -o '^\d+')" 43 | fi 44 | 45 | elif [[ $(uname -s) == "Darwin" ]]; then 46 | echo "To use Docker in OS X, you need to install Docker for Mac" 47 | echo "or Dlite: https://github.com/nlf/dlite" 48 | exit 1 49 | fi 50 | 51 | args="" 52 | 53 | if [[ -z $PHAROD_FOREGROUND ]]; then 54 | args="$args -d" 55 | fi 56 | 57 | exec sudo sh -c "DOCKER_HOST='unix:///var/run/docker.sock' DOCKER_HOST_IP='$DOCKER_HOST_IP' DOCKER_FIRST_EPHEMERAL_PORT='$DOCKER_FIRST_EPHEMERAL_PORT' '$pharod' $args" 58 | -------------------------------------------------------------------------------- /pharod-stop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | sudo pkill -x pharod 5 | echo "Stopped Pharod" -------------------------------------------------------------------------------- /pharodctl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "os" 8 | "time" 9 | ) 10 | 11 | func die(msg string) { 12 | fmt.Println(msg) 13 | os.Exit(1) 14 | } 15 | 16 | func usage() { 17 | die("usage: pharodctl ls") 18 | } 19 | 20 | func main() { 21 | if len(os.Args) < 2 { 22 | usage() 23 | } 24 | 25 | client, err := net.Dial("unix", "/tmp/pharod.sock") 26 | if err != nil { 27 | die(err.Error()) 28 | } 29 | defer client.Close() 30 | client.SetDeadline(time.Now().Add(5 * time.Second)) 31 | 32 | _, err = client.Write([]byte(os.Args[1] + "\n")) 33 | if err != nil { 34 | die(err.Error()) 35 | } 36 | 37 | clientReader := bufio.NewReader(client) 38 | for { 39 | line, err := clientReader.ReadString('\n') 40 | if err != nil { 41 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() { 42 | die(err.Error()) 43 | } else { 44 | break 45 | } 46 | } 47 | fmt.Print(line) 48 | } 49 | } 50 | --------------------------------------------------------------------------------