| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- // The natc command is a work-in-progress implementation of a NAT based
- // connector for Tailscale. It is intended to be used to route traffic to a
- // specific domain through a specific node.
- package main
- import (
- "context"
- "encoding/json"
- "errors"
- "expvar"
- "flag"
- "fmt"
- "log"
- "math/rand/v2"
- "net"
- "net/http"
- "net/netip"
- "os"
- "path/filepath"
- "strings"
- "time"
- "github.com/gaissmai/bart"
- "github.com/hashicorp/raft"
- "github.com/inetaf/tcpproxy"
- "github.com/peterbourgon/ff/v3"
- "go4.org/netipx"
- "golang.org/x/net/dns/dnsmessage"
- "tailscale.com/client/local"
- "tailscale.com/client/tailscale/apitype"
- "tailscale.com/cmd/natc/ippool"
- "tailscale.com/envknob"
- "tailscale.com/hostinfo"
- "tailscale.com/ipn"
- "tailscale.com/net/netutil"
- "tailscale.com/tsnet"
- "tailscale.com/tsweb"
- "tailscale.com/util/mak"
- "tailscale.com/util/must"
- "tailscale.com/wgengine/netstack"
- )
- func main() {
- hostinfo.SetApp("natc")
- if !envknob.UseWIPCode() {
- log.Fatal("cmd/natc is a work in progress and has not been security reviewed;\nits use requires TAILSCALE_USE_WIP_CODE=1 be set in the environment for now.")
- }
- // Parse flags
- fs := flag.NewFlagSet("natc", flag.ExitOnError)
- var (
- debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
- hostname = fs.String("hostname", "", "Hostname to register the service under")
- siteID = fs.Uint("site-id", 1, "an integer site ID to use for the ULA prefix which allows for multiple proxies to act in a HA configuration")
- v4PfxStr = fs.String("v4-pfx", "100.64.1.0/24", "comma-separated list of IPv4 prefixes to advertise")
- dnsServers = fs.String("dns-servers", "", "comma separated list of upstream DNS to use, including host and port (use system if empty)")
- verboseTSNet = fs.Bool("verbose-tsnet", false, "enable verbose logging in tsnet")
- printULA = fs.Bool("print-ula", false, "print the ULA prefix and exit")
- ignoreDstPfxStr = fs.String("ignore-destinations", "", "comma-separated list of prefixes to ignore")
- wgPort = fs.Uint("wg-port", 0, "udp port for wireguard and peer to peer traffic")
- clusterTag = fs.String("cluster-tag", "", "optionally run in a consensus cluster with other nodes with this tag")
- server = fs.String("login-server", ipn.DefaultControlURL, "the base URL of control server")
- stateDir = fs.String("state-dir", "", "path to directory in which to store app state")
- clusterFollowOnly = fs.Bool("follow-only", false, "Try to find a leader with the cluster tag or exit.")
- clusterAdminPort = fs.Int("cluster-admin-port", 8081, "Port on localhost for the cluster admin HTTP API")
- )
- ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_NATC"))
- if *printULA {
- fmt.Println(ula(uint16(*siteID)))
- return
- }
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- if *siteID == 0 {
- log.Fatalf("site-id must be set")
- } else if *siteID > 0xffff {
- log.Fatalf("site-id must be in the range [0, 65535]")
- }
- var ignoreDstTable *bart.Table[bool]
- for s := range strings.SplitSeq(*ignoreDstPfxStr, ",") {
- s := strings.TrimSpace(s)
- if s == "" {
- continue
- }
- if ignoreDstTable == nil {
- ignoreDstTable = &bart.Table[bool]{}
- }
- pfx, err := netip.ParsePrefix(s)
- if err != nil {
- log.Fatalf("unable to parse prefix: %v", err)
- }
- if pfx.Masked() != pfx {
- log.Fatalf("prefix %v is not normalized (bits are set outside the mask)", pfx)
- }
- ignoreDstTable.Insert(pfx, true)
- }
- ts := &tsnet.Server{
- Hostname: *hostname,
- Dir: *stateDir,
- }
- ts.ControlURL = *server
- if *wgPort != 0 {
- if *wgPort >= 1<<16 {
- log.Fatalf("wg-port must be in the range [0, 65535]")
- }
- ts.Port = uint16(*wgPort)
- }
- defer ts.Close()
- if *verboseTSNet {
- ts.Logf = log.Printf
- }
- // Start special-purpose listeners: dns, http promotion, debug server
- if *debugPort != 0 {
- mux := http.NewServeMux()
- tsweb.Debugger(mux)
- dln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *debugPort))
- if err != nil {
- log.Fatalf("failed listening on debug port: %v", err)
- }
- defer dln.Close()
- go func() {
- log.Fatalf("debug serve: %v", http.Serve(dln, mux))
- }()
- }
- if err := ts.Start(); err != nil {
- log.Fatalf("ts.Start: %v", err)
- }
- // TODO(raggi): this is not a public interface or guarantee.
- ns := ts.Sys().Netstack.Get().(*netstack.Impl)
- if *debugPort != 0 {
- expvar.Publish("netstack", ns.ExpVar())
- }
- lc, err := ts.LocalClient()
- if err != nil {
- log.Fatalf("LocalClient() failed: %v", err)
- }
- if _, err := ts.Up(ctx); err != nil {
- log.Fatalf("ts.Up: %v", err)
- }
- var prefixes []netip.Prefix
- for _, s := range strings.Split(*v4PfxStr, ",") {
- p := netip.MustParsePrefix(strings.TrimSpace(s))
- if p.Masked() != p {
- log.Fatalf("v4 prefix %v is not a masked prefix", p)
- }
- prefixes = append(prefixes, p)
- }
- routes, dnsAddr, addrPool := calculateAddresses(prefixes)
- v6ULA := ula(uint16(*siteID))
- var ipp ippool.IPPool
- if *clusterTag != "" {
- cipp := ippool.NewConsensusIPPool(addrPool)
- clusterStateDir, err := getClusterStatePath(*stateDir)
- if err != nil {
- log.Fatalf("Creating cluster state dir failed: %v", err)
- }
- err = cipp.StartConsensus(ctx, ts, ippool.ClusterOpts{
- Tag: *clusterTag,
- StateDir: clusterStateDir,
- FollowOnly: *clusterFollowOnly,
- })
- if err != nil {
- log.Fatalf("StartConsensus: %v", err)
- }
- defer func() {
- err := cipp.StopConsensus(ctx)
- if err != nil {
- log.Printf("Error stopping consensus: %v", err)
- }
- }()
- ipp = cipp
- go func() {
- // This listens on localhost only, so that only those with access to the host machine
- // can remove servers from the cluster config.
- log.Print(http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", *clusterAdminPort), httpClusterAdmin(cipp)))
- }()
- } else {
- ipp = &ippool.SingleMachineIPPool{IPSet: addrPool}
- }
- c := &connector{
- ts: ts,
- whois: lc,
- v6ULA: v6ULA,
- ignoreDsts: ignoreDstTable,
- ipPool: ipp,
- routes: routes,
- dnsAddr: dnsAddr,
- resolver: getResolver(*dnsServers),
- }
- c.run(ctx, lc)
- }
- // getResolver parses serverFlag and returns either the default resolver, or a
- // resolver that uses the provided comma-separated DNS server AddrPort's, or
- // panics.
- func getResolver(serverFlag string) lookupNetIPer {
- if serverFlag == "" {
- return net.DefaultResolver
- }
- var addrs []string
- for s := range strings.SplitSeq(serverFlag, ",") {
- s = strings.TrimSpace(s)
- addr, err := netip.ParseAddrPort(s)
- if err != nil {
- log.Fatalf("dns server provided: %q does not parse: %v", s, err)
- }
- addrs = append(addrs, addr.String())
- }
- return &net.Resolver{
- PreferGo: true,
- Dial: func(ctx context.Context, network string, address string) (net.Conn, error) {
- var dialer net.Dialer
- // TODO(raggi): perhaps something other than random?
- return dialer.DialContext(ctx, network, addrs[rand.N(len(addrs))])
- },
- }
- }
- func calculateAddresses(prefixes []netip.Prefix) (*netipx.IPSet, netip.Addr, *netipx.IPSet) {
- var ipsb netipx.IPSetBuilder
- for _, p := range prefixes {
- ipsb.AddPrefix(p)
- }
- routesToAdvertise := must.Get(ipsb.IPSet())
- dnsAddr := routesToAdvertise.Ranges()[0].From()
- ipsb.Remove(dnsAddr)
- addrPool := must.Get(ipsb.IPSet())
- return routesToAdvertise, dnsAddr, addrPool
- }
- type lookupNetIPer interface {
- LookupNetIP(ctx context.Context, net, host string) ([]netip.Addr, error)
- }
- type whoiser interface {
- WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error)
- }
- type connector struct {
- // ts is the tsnet.Server used to host the connector.
- ts *tsnet.Server
- // whois is the local.Client used to interact with the tsnet.Server hosting this
- // connector.
- whois whoiser
- // dnsAddr is the IPv4 address to listen on for DNS requests. It is used to
- // prevent the app connector from assigning it to a domain.
- dnsAddr netip.Addr
- // routes is the set of IPv4 ranges advertised to the tailnet, or ipset with
- // the dnsAddr removed.
- routes *netipx.IPSet
- // v6ULA is the ULA prefix used by the app connector to assign IPv6 addresses.
- v6ULA netip.Prefix
- // ignoreDsts is initialized at start up with the contents of --ignore-destinations (if none it is nil)
- // It is never mutated, only used for lookups.
- // Users who want to natc a DNS wildcard but not every address record in that domain can supply the
- // exceptions in --ignore-destinations. When we receive a dns request we will look up the fqdn
- // and if any of the ip addresses in response to the lookup match any 'ignore destinations' prefix we will
- // return a dns response that contains the ip addresses we discovered with the lookup (ie not the
- // natc behavior, which would return a dummy ip address pointing at natc).
- ignoreDsts *bart.Table[bool]
- // ipPool contains the per-peer IPv4 address assignments.
- ipPool ippool.IPPool
- // resolver is used to lookup IP addresses for DNS queries.
- resolver lookupNetIPer
- }
- // v6ULA is the ULA prefix used by the app connector to assign IPv6 addresses.
- // The 8th and 9th bytes are used to encode the site ID which allows for
- // multiple proxies to act in a HA configuration.
- // mnemonic: a99c = appc
- var v6ULA = netip.MustParsePrefix("fd7a:115c:a1e0:a99c::/64")
- func ula(siteID uint16) netip.Prefix {
- as16 := v6ULA.Addr().As16()
- as16[8] = byte(siteID >> 8)
- as16[9] = byte(siteID)
- return netip.PrefixFrom(netip.AddrFrom16(as16), 64+16)
- }
- // run runs the connector.
- //
- // The passed in context is only used for the initial setup. The connector runs
- // forever.
- func (c *connector) run(ctx context.Context, lc *local.Client) {
- if _, err := lc.EditPrefs(ctx, &ipn.MaskedPrefs{
- AdvertiseRoutesSet: true,
- Prefs: ipn.Prefs{
- AdvertiseRoutes: append(c.routes.Prefixes(), c.v6ULA),
- },
- }); err != nil {
- log.Fatalf("failed to advertise routes: %v", err)
- }
- c.ts.RegisterFallbackTCPHandler(c.handleTCPFlow)
- c.serveDNS()
- }
- func (c *connector) serveDNS() {
- pc, err := c.ts.ListenPacket("udp", net.JoinHostPort(c.dnsAddr.String(), "53"))
- if err != nil {
- log.Fatalf("failed listening on port 53: %v", err)
- }
- defer pc.Close()
- log.Printf("Listening for DNS on %s", pc.LocalAddr().String())
- for {
- buf := make([]byte, 1500)
- n, addr, err := pc.ReadFrom(buf)
- if err != nil {
- if errors.Is(err, net.ErrClosed) {
- return
- }
- log.Printf("serveDNS.ReadFrom failed: %v", err)
- continue
- }
- go c.handleDNS(pc, buf[:n], addr.(*net.UDPAddr))
- }
- }
- // handleDNS handles a DNS request to the app connector.
- // It generates a response based on the request and the node that sent it.
- //
- // Each node is assigned a unique pair of IP addresses for each domain it
- // queries. This assignment is done lazily and is not persisted across restarts.
- // A per-peer assignment allows the connector to reuse a limited number of IP
- // addresses across multiple nodes and domains. It also allows for clear
- // failover behavior when an app connector is restarted.
- //
- // This assignment later allows the connector to determine where to forward
- // traffic based on the destination IP address.
- func (c *connector) handleDNS(pc net.PacketConn, buf []byte, remoteAddr *net.UDPAddr) {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- who, err := c.whois.WhoIs(ctx, remoteAddr.String())
- if err != nil {
- log.Printf("HandleDNS(remote=%s): WhoIs failed: %v\n", remoteAddr.String(), err)
- return
- }
- var msg dnsmessage.Message
- err = msg.Unpack(buf)
- if err != nil {
- log.Printf("HandleDNS(remote=%s): dnsmessage unpack failed: %v\n", remoteAddr.String(), err)
- return
- }
- var resolves map[string][]netip.Addr
- var addrQCount int
- for _, q := range msg.Questions {
- if q.Type != dnsmessage.TypeA && q.Type != dnsmessage.TypeAAAA {
- continue
- }
- addrQCount++
- if _, ok := resolves[q.Name.String()]; !ok {
- addrs, err := c.resolver.LookupNetIP(ctx, "ip", q.Name.String())
- var dnsErr *net.DNSError
- if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
- continue
- }
- if err != nil {
- log.Printf("HandleDNS(remote=%s): lookup destination failed: %v\n", remoteAddr.String(), err)
- return
- }
- // Note: If _any_ destination is ignored, pass through all of the resolved
- // addresses as-is.
- //
- // This could result in some odd split-routing if there was a mix of
- // ignored and non-ignored addresses, but it's currently the user
- // preferred behavior.
- if !c.ignoreDestination(addrs) {
- addr, err := c.ipPool.IPForDomain(who.Node.ID, q.Name.String())
- if err != nil {
- log.Printf("HandleDNS(remote=%s): lookup destination failed: %v\n", remoteAddr.String(), err)
- return
- }
- addrs = []netip.Addr{addr, v6ForV4(c.v6ULA.Addr(), addr)}
- }
- mak.Set(&resolves, q.Name.String(), addrs)
- }
- }
- rcode := dnsmessage.RCodeSuccess
- if addrQCount > 0 && len(resolves) == 0 {
- rcode = dnsmessage.RCodeNameError
- }
- b := dnsmessage.NewBuilder(nil,
- dnsmessage.Header{
- ID: msg.Header.ID,
- Response: true,
- Authoritative: true,
- RCode: rcode,
- })
- b.EnableCompression()
- if err := b.StartQuestions(); err != nil {
- log.Printf("HandleDNS(remote=%s): dnsmessage start questions failed: %v\n", remoteAddr.String(), err)
- return
- }
- for _, q := range msg.Questions {
- b.Question(q)
- }
- if err := b.StartAnswers(); err != nil {
- log.Printf("HandleDNS(remote=%s): dnsmessage start answers failed: %v\n", remoteAddr.String(), err)
- return
- }
- for _, q := range msg.Questions {
- switch q.Type {
- case dnsmessage.TypeSOA:
- if err := b.SOAResource(
- dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
- dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
- Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
- ); err != nil {
- log.Printf("HandleDNS(remote=%s): dnsmessage SOA resource failed: %v\n", remoteAddr.String(), err)
- return
- }
- case dnsmessage.TypeNS:
- if err := b.NSResource(
- dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
- dnsmessage.NSResource{NS: tsMBox},
- ); err != nil {
- log.Printf("HandleDNS(remote=%s): dnsmessage NS resource failed: %v\n", remoteAddr.String(), err)
- return
- }
- case dnsmessage.TypeAAAA:
- for _, addr := range resolves[q.Name.String()] {
- if !addr.Is6() {
- continue
- }
- if err := b.AAAAResource(
- dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
- dnsmessage.AAAAResource{AAAA: addr.As16()},
- ); err != nil {
- log.Printf("HandleDNS(remote=%s): dnsmessage AAAA resource failed: %v\n", remoteAddr.String(), err)
- return
- }
- }
- case dnsmessage.TypeA:
- for _, addr := range resolves[q.Name.String()] {
- if !addr.Is4() {
- continue
- }
- if err := b.AResource(
- dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
- dnsmessage.AResource{A: addr.As4()},
- ); err != nil {
- log.Printf("HandleDNS(remote=%s): dnsmessage A resource failed: %v\n", remoteAddr.String(), err)
- return
- }
- }
- }
- }
- out, err := b.Finish()
- if err != nil {
- log.Printf("HandleDNS(remote=%s): dnsmessage finish failed: %v\n", remoteAddr.String(), err)
- return
- }
- _, err = pc.WriteTo(out, remoteAddr)
- if err != nil {
- log.Printf("HandleDNS(remote=%s): write failed: %v\n", remoteAddr.String(), err)
- }
- }
- func v6ForV4(ula netip.Addr, v4 netip.Addr) netip.Addr {
- as16 := ula.As16()
- as4 := v4.As4()
- copy(as16[12:], as4[:])
- return netip.AddrFrom16(as16)
- }
- func v4ForV6(v6 netip.Addr) netip.Addr {
- as16 := v6.As16()
- var as4 [4]byte
- copy(as4[:], as16[12:])
- return netip.AddrFrom4(as4)
- }
- // tsMBox is the mailbox used in SOA records.
- // The convention is to replace the @ symbol with a dot.
- // So in this case, the mailbox is support.tailscale.com. with the trailing dot
- // to indicate that it is a fully qualified domain name.
- var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
- // handleTCPFlow handles a TCP flow from the given source to the given
- // destination. It uses the source address to determine the node that sent the
- // request and the destination address to determine the domain that the request
- // is for based on the IP address assigned to the destination in the DNS
- // response.
- func (c *connector) handleTCPFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- who, err := c.whois.WhoIs(ctx, src.Addr().String())
- cancel()
- if err != nil {
- log.Printf("HandleTCPFlow: WhoIs failed: %v\n", err)
- return nil, false
- }
- dstAddr := dst.Addr()
- if dstAddr.Is6() {
- dstAddr = v4ForV6(dstAddr)
- }
- domain, ok := c.ipPool.DomainForIP(who.Node.ID, dstAddr, time.Now())
- if !ok {
- return nil, false
- }
- return func(conn net.Conn) {
- proxyTCPConn(conn, domain, c)
- }, true
- }
- // ignoreDestination reports whether any of the provided dstAddrs match the prefixes configured
- // in --ignore-destinations
- func (c *connector) ignoreDestination(dstAddrs []netip.Addr) bool {
- if c.ignoreDsts == nil {
- return false
- }
- for _, a := range dstAddrs {
- if _, ok := c.ignoreDsts.Lookup(a); ok {
- return true
- }
- }
- return false
- }
- func proxyTCPConn(c net.Conn, dest string, ctor *connector) {
- if c.RemoteAddr() == nil {
- log.Printf("proxyTCPConn: nil RemoteAddr")
- c.Close()
- return
- }
- laddr, err := netip.ParseAddrPort(c.LocalAddr().String())
- if err != nil {
- log.Printf("proxyTCPConn: ParseAddrPort failed: %v", err)
- c.Close()
- return
- }
- daddrs, err := ctor.resolver.LookupNetIP(context.TODO(), "ip", dest)
- if err != nil {
- log.Printf("proxyTCPConn: LookupNetIP failed: %v", err)
- c.Close()
- return
- }
- if len(daddrs) == 0 {
- log.Printf("proxyTCPConn: no IP addresses found for %s", dest)
- c.Close()
- return
- }
- if ctor.ignoreDestination(daddrs) {
- log.Printf("proxyTCPConn: closing connection to ignored destination %s (%v)", dest, daddrs)
- c.Close()
- return
- }
- p := &tcpproxy.Proxy{
- ListenFunc: func(net, laddr string) (net.Listener, error) {
- return netutil.NewOneConnListener(c, nil), nil
- },
- }
- // TODO(raggi): more code could avoid this shuffle, but avoiding allocations
- // for now most of the time daddrs will be short.
- rand.Shuffle(len(daddrs), func(i, j int) {
- daddrs[i], daddrs[j] = daddrs[j], daddrs[i]
- })
- daddr := daddrs[0]
- // Try to match the upstream and downstream protocols (v4/v6)
- if laddr.Addr().Is6() {
- for _, addr := range daddrs {
- if addr.Is6() {
- daddr = addr
- break
- }
- }
- } else {
- for _, addr := range daddrs {
- if addr.Is4() {
- daddr = addr
- break
- }
- }
- }
- // TODO(raggi): drop this library, it ends up being allocation and
- // indirection heavy and really doesn't help us here.
- dsockaddrs := netip.AddrPortFrom(daddr, laddr.Port()).String()
- p.AddRoute(dsockaddrs, &tcpproxy.DialProxy{
- Addr: dsockaddrs,
- })
- p.Start()
- }
- func getClusterStatePath(stateDirFlag string) (string, error) {
- var dirPath string
- if stateDirFlag != "" {
- dirPath = stateDirFlag
- } else {
- confDir, err := os.UserConfigDir()
- if err != nil {
- return "", err
- }
- dirPath = filepath.Join(confDir, "nat-connector-state")
- }
- dirPath = filepath.Join(dirPath, "cluster")
- if err := os.MkdirAll(dirPath, 0700); err != nil {
- return "", err
- }
- if fi, err := os.Stat(dirPath); err != nil {
- return "", err
- } else if !fi.IsDir() {
- return "", fmt.Errorf("%v is not a directory", dirPath)
- }
- return dirPath, nil
- }
- func httpClusterAdmin(ipp *ippool.ConsensusIPPool) http.Handler {
- mux := http.NewServeMux()
- mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
- c, err := ipp.GetClusterConfiguration()
- if err != nil {
- log.Printf("cluster admin http: error getClusterConfig: %v", err)
- http.Error(w, "", http.StatusInternalServerError)
- return
- }
- if err := json.NewEncoder(w).Encode(c); err != nil {
- log.Printf("cluster admin http: error encoding raft configuration: %v", err)
- }
- })
- mux.HandleFunc("DELETE /{id}", func(w http.ResponseWriter, r *http.Request) {
- idString := r.PathValue("id")
- id := raft.ServerID(idString)
- idx, err := ipp.DeleteClusterServer(id)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if err := json.NewEncoder(w).Encode(idx); err != nil {
- log.Printf("cluster admin http: error encoding delete index: %v", err)
- return
- }
- })
- return mux
- }
|