client.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. package dclient
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "log"
  8. "net"
  9. "os"
  10. "strconv"
  11. "strings"
  12. "git.aetherial.dev/aeth/yosai/pkg/cloud/linode"
  13. "git.aetherial.dev/aeth/yosai/pkg/daemon"
  14. "git.aetherial.dev/aeth/yosai/pkg/secrets/hashicorp"
  15. "git.aetherial.dev/aeth/yosai/pkg/semaphore"
  16. )
  17. type DaemonClient struct {
  18. SockPath string // the absolute path of the unix domain socket
  19. Stream io.ReadWriter
  20. }
  21. const BLANK_JSON = "{\"blank\": \"hey!\"}"
  22. /*
  23. Gets the configuration from the upstream daemon/server
  24. */
  25. func (d DaemonClient) GetConfig() daemon.ConfigFromFile {
  26. resp := d.Call([]byte(BLANK_JSON), "config", "show")
  27. var cfg daemon.ConfigFromFile
  28. err := json.Unmarshal(resp.Body, &cfg)
  29. if err != nil {
  30. log.Fatal("error unmarshalling config struct ", err.Error())
  31. }
  32. return cfg
  33. }
  34. /*
  35. Parse a string into a hashmap to allow for key-based data retrieval. Strings formatted in a
  36. comma delimited, key-value pair denoted by an equal sign can be parsed. i.e. 'name=primary,wan=8.8.8.8'
  37. :param arg: the string to be parsed
  38. */
  39. func makeArgMap(arg string) map[string]string {
  40. argSplit := strings.Split(arg, ",")
  41. argMap := map[string]string{}
  42. for i := range argSplit {
  43. a := strings.Split(strings.TrimSpace(argSplit[i]), "=")
  44. if len(a) != 2 {
  45. log.Fatal("Key values must be passed comma delimmited, seperated with an '='. i.e. 'public=12345abcdef,secret=qwerty69420'")
  46. }
  47. argMap[strings.TrimSpace(strings.ToLower(a[0]))] = strings.TrimSpace(a[1])
  48. }
  49. return argMap
  50. }
  51. /*
  52. Convenience function for building a request to add a server to the
  53. daemon's configuration table
  54. :param argMap: a map with named elements that correspond to the subsequent struct's fields
  55. */
  56. func serverAddRequestBuilder(argMap map[string]string) []byte {
  57. port, err := strconv.Atoi(argMap["port"])
  58. if err != nil {
  59. log.Fatal("Port passed: ", argMap["port"], " is not a valid integer.")
  60. }
  61. if port <= 0 || port > 65535 {
  62. log.Fatal("Port passed: ", port, " Was not in the valid range of between 1-65535.")
  63. }
  64. b, _ := json.Marshal(daemon.VpnServer{WanIpv4: argMap["wan"], Port: port, Name: argMap["name"]})
  65. return b
  66. }
  67. /*
  68. Wraps the creation of a request to add/delete a peer from the config
  69. :param argMap: a map with named elements that correspond to the subsequent struct's fields
  70. */
  71. func peerAddRequestBuilder(argMap map[string]string) []byte {
  72. b, _ := json.Marshal(daemon.VpnClient{Name: argMap["name"], Pubkey: argMap["pubkey"]})
  73. return b
  74. }
  75. /*
  76. Wraps the creation of a request to add to the keyring
  77. :param argMap: a map with named elements that correspond to the subsequent struct's fields
  78. */
  79. func keyringRequstBuilder(argMap map[string]string) []byte {
  80. b, _ := json.Marshal(hashicorp.VaultItem{Public: argMap["public"], Secret: argMap["secret"], Type: argMap["type"], Name: argMap["name"]})
  81. return b
  82. }
  83. /*
  84. Wraps the creation of a request to render a configuration
  85. :param argMap: a map with named elements that correspond to the subsequent struct's fields
  86. */
  87. func configRenderRequestBuilder(argMap map[string]string) []byte {
  88. b, _ := json.Marshal(daemon.ConfigRenderRequest{Server: argMap["server"], Client: argMap["client"]})
  89. return b
  90. }
  91. func (d DaemonClient) addLinodeRequestBuilder(arg string) []byte {
  92. cfg := d.GetConfig()
  93. addLn := linode.AddLinodeRequest{
  94. Name: arg,
  95. Image: cfg.Cloud.Image,
  96. Type: cfg.Cloud.LinodeType,
  97. Region: cfg.Cloud.Region,
  98. }
  99. b, _ := json.Marshal(addLn)
  100. return b
  101. }
  102. func (d DaemonClient) Call(payload []byte, target string, method string) daemon.SockMessage {
  103. msg := daemon.SockMessage{
  104. Type: daemon.MsgRequest,
  105. TypeLen: int8(len(daemon.MsgRequest)),
  106. StatusMsg: "",
  107. StatusCode: 0,
  108. Version: daemon.SockMsgVers,
  109. Body: payload,
  110. Target: target,
  111. Method: method,
  112. }
  113. conn, err := net.Dial("unix", d.SockPath)
  114. if err != nil {
  115. log.Fatal(err)
  116. }
  117. defer conn.Close()
  118. buf := bytes.NewBuffer(daemon.Marshal(msg))
  119. _, err = io.Copy(conn, buf)
  120. if err != nil {
  121. log.Fatal("write error:", err)
  122. }
  123. resp := bytes.NewBuffer([]byte{})
  124. _, err = io.Copy(resp, conn)
  125. if err != nil {
  126. if err == io.EOF {
  127. fmt.Println("exited ok.")
  128. os.Exit(0)
  129. }
  130. log.Fatal(err)
  131. }
  132. return daemon.Unmarshal(resp.Bytes())
  133. }
  134. const UNIX_DOMAIN_SOCK_PATH = "/tmp/yosaid.sock"
  135. /*
  136. Build a JSON request to send the yosaid daemon
  137. :param v: a struct to serialize for a request
  138. :param value: a string to put into the request
  139. */
  140. func jsonBuilder(v interface{}, value string) []byte {
  141. delLn, ok := v.(linode.DeleteLinodeRequest)
  142. if ok {
  143. delLn = linode.DeleteLinodeRequest{
  144. Name: value,
  145. }
  146. b, _ := json.Marshal(delLn)
  147. return b
  148. }
  149. pollLn, ok := v.(linode.PollLinodeRequest)
  150. if ok {
  151. pollLn = linode.PollLinodeRequest{
  152. Address: value,
  153. }
  154. b, _ := json.Marshal(pollLn)
  155. return b
  156. }
  157. semReq, ok := v.(semaphore.SemaphoreRequest)
  158. if ok {
  159. semReq = semaphore.SemaphoreRequest{
  160. Target: value,
  161. }
  162. b, _ := json.Marshal(semReq)
  163. return b
  164. }
  165. return []byte("{\"data\":\"test\"}")
  166. }
  167. /*
  168. Create a server, and propogate it across the daemon's system
  169. */
  170. func (d DaemonClient) NewServer(name string) error {
  171. // create new server in cloud environment
  172. ipv4, err := d.addLinode(name)
  173. if err != nil {
  174. return err
  175. }
  176. // add server data to daemon configuration
  177. conf := d.GetConfig()
  178. b, _ := json.Marshal(daemon.VpnServer{WanIpv4: ipv4, Name: name, Port: conf.Service.VpnServerPort})
  179. resp := d.Call(b, "config-server", "add")
  180. if resp.StatusCode != daemon.REQUEST_OK {
  181. return &DaemonClientError{SockMsg: resp}
  182. }
  183. // add configuration data to ansible
  184. resp = d.Call(jsonBuilder(semaphore.SemaphoreRequest{}, name), "ansible-hosts", "add")
  185. if resp.StatusCode != daemon.REQUEST_OK {
  186. return &DaemonClientError{SockMsg: resp}
  187. }
  188. return nil
  189. }
  190. /*
  191. Helper function to get servers from the daemon config
  192. :param val: either the WAN IPv4 address, or the name of the server to get
  193. */
  194. func (d DaemonClient) GetServer(val string) (daemon.VpnServer, error) {
  195. cfg := d.GetConfig()
  196. for name := range cfg.Service.Servers {
  197. if cfg.Service.Servers[name].WanIpv4 == val {
  198. return cfg.Service.Servers[val], nil
  199. }
  200. server, ok := cfg.Service.Servers[val]
  201. if ok {
  202. return server, nil
  203. }
  204. }
  205. return daemon.VpnServer{}, &ServerNotFound{Name: val}
  206. }
  207. /*
  208. Add a server to the configuration
  209. */
  210. func (d DaemonClient) AddServeToConfig(val string) error {
  211. argMap := makeArgMap(val)
  212. resp := d.Call(serverAddRequestBuilder(argMap), "config-server", "add")
  213. if resp.StatusCode != daemon.REQUEST_OK {
  214. return &DaemonClientError{SockMsg: resp}
  215. }
  216. return nil
  217. }
  218. /*
  219. Trigger the daemon to execute the vpn rotation playbook on all of the servers in the ansible inventory
  220. */
  221. func (d DaemonClient) ConfigureServers() (daemon.SockMessage, error) {
  222. resp := d.Call(jsonBuilder(semaphore.SemaphoreRequest{}, semaphore.YosaiVpnRotationJob), "ansible-job", "run")
  223. if resp.StatusCode != daemon.REQUEST_OK {
  224. return resp, &DaemonClientError{SockMsg: resp}
  225. }
  226. taskInfo := semaphore.TaskInfo{}
  227. err := json.Unmarshal(resp.Body, &taskInfo)
  228. if err != nil {
  229. return resp, &DaemonClientError{SockMsg: resp}
  230. }
  231. resp = d.Call(jsonBuilder(semaphore.SemaphoreRequest{}, fmt.Sprint(taskInfo.ID)), "ansible-job", "poll")
  232. if resp.StatusCode != daemon.REQUEST_OK {
  233. return resp, &DaemonClientError{SockMsg: resp}
  234. }
  235. return resp, nil
  236. }
  237. /*
  238. Poll until a server is done being created
  239. :param name: the name of the server
  240. */
  241. func (d DaemonClient) PollServer(name string) (daemon.SockMessage, error) {
  242. resp := d.Call(jsonBuilder(linode.PollLinodeRequest{}, name), "cloud", "poll")
  243. if resp.StatusCode != daemon.REQUEST_OK {
  244. return resp, &DaemonClientError{SockMsg: resp}
  245. }
  246. return resp, nil
  247. }
  248. /*
  249. Remove a server from the daemon configuration
  250. :param name: the name of the server to remove
  251. */
  252. func (d DaemonClient) RemoveServerFromConfig(name string) error {
  253. b, _ := json.Marshal(daemon.VpnServer{Name: name})
  254. resp := d.Call(b, "config-server", "delete")
  255. if resp.StatusCode != daemon.REQUEST_OK {
  256. return &DaemonClientError{SockMsg: resp}
  257. }
  258. return nil
  259. }
  260. /*
  261. Remove a server from the ansible inventory
  262. :param name: the name of the server to remove from ansible
  263. */
  264. func (d DaemonClient) RemoveServerFromAnsible(name string) error {
  265. server, err := d.GetServer(name)
  266. if err != nil {
  267. return err
  268. }
  269. resp := d.Call(jsonBuilder(semaphore.SemaphoreRequest{}, server.WanIpv4), "ansible-hosts", "delete")
  270. if resp.StatusCode != daemon.REQUEST_OK {
  271. return &DaemonClientError{SockMsg: resp}
  272. }
  273. return nil
  274. }
  275. /*
  276. Destroy a server by its logical name in the configuration, ansible inventory, and cloud provider
  277. :param name: the name of the server in the system
  278. */
  279. func (d DaemonClient) DestroyServer(name string) error {
  280. cfg := d.GetConfig()
  281. resp := d.Call(jsonBuilder(linode.DeleteLinodeRequest{}, name), "cloud", "delete")
  282. if resp.StatusCode != daemon.REQUEST_OK {
  283. return &DaemonClientError{SockMsg: resp}
  284. }
  285. resp = d.Call(jsonBuilder(semaphore.SemaphoreRequest{}, cfg.Service.Servers[name].WanIpv4), "ansible-hosts", "delete")
  286. if resp.StatusCode != daemon.REQUEST_OK {
  287. return &DaemonClientError{SockMsg: resp}
  288. }
  289. b, _ := json.Marshal(daemon.VpnServer{Name: name})
  290. resp = d.Call(b, "config-server", "delete")
  291. if resp.StatusCode != daemon.REQUEST_OK {
  292. return &DaemonClientError{SockMsg: resp}
  293. }
  294. return nil
  295. }
  296. func (d DaemonClient) BringDownIntf(name string) (daemon.SockMessage, error) {
  297. b, _ := json.Marshal(daemon.StartWireguardRequest{InterfaceName: name})
  298. resp := d.Call(b, "daemon", "wg-down")
  299. if resp.StatusCode != daemon.REQUEST_OK {
  300. return resp, &DaemonClientError{SockMsg: resp}
  301. }
  302. return resp, nil
  303. }
  304. func (d DaemonClient) BringUpIntf(name string) (daemon.SockMessage, error) {
  305. b, _ := json.Marshal(daemon.StartWireguardRequest{InterfaceName: name})
  306. resp := d.Call(b, "daemon", "wg-up")
  307. if resp.StatusCode != daemon.REQUEST_OK {
  308. return resp, &DaemonClientError{SockMsg: resp}
  309. }
  310. return resp, nil
  311. }
  312. func (d DaemonClient) DestroyIntf(name string) error {
  313. return nil
  314. }
  315. func (d DaemonClient) HealthCheck() (daemon.SockMessage, error) {
  316. return daemon.SockMessage{}, nil
  317. }
  318. func (d DaemonClient) LockFirewall() error {
  319. return nil
  320. }
  321. /*
  322. Render the a wireguard configuration file
  323. */
  324. func (d DaemonClient) RenderWgConfig(arg string) daemon.SockMessage {
  325. argMap := makeArgMap(arg)
  326. outputToFile, ok := argMap["outmode"]
  327. var outToFile bool
  328. if ok {
  329. if outputToFile == "save" {
  330. outToFile = true
  331. } else {
  332. outToFile = false
  333. }
  334. } else {
  335. outToFile = false
  336. }
  337. b, _ := json.Marshal(daemon.ConfigRenderRequest{Server: argMap["server"], Client: argMap["client"], OutputToFile: outToFile})
  338. return d.Call(b, "daemon", "render-config")
  339. }
  340. /*
  341. This is function creates a linode, and then returns the IPv4 as a string
  342. :param name: the name to assign the linode
  343. */
  344. func (d DaemonClient) addLinode(name string) (string, error) {
  345. cfg := d.GetConfig()
  346. b, _ := json.Marshal(linode.AddLinodeRequest{
  347. Image: cfg.Cloud.Image,
  348. Region: cfg.Cloud.Region,
  349. Type: cfg.Cloud.LinodeType,
  350. Name: name,
  351. })
  352. resp := d.Call(b, "cloud", "add")
  353. linodeResp := linode.GetLinodeResponse{}
  354. err := json.Unmarshal(resp.Body, &linodeResp)
  355. if err != nil {
  356. return "", &DaemonClientError{SockMsg: resp}
  357. }
  358. return linodeResp.Ipv4[0], nil
  359. }
  360. func (d DaemonClient) BootstrapAll() error {
  361. resp := d.Call(jsonBuilder(semaphore.SemaphoreRequest{}, "all"), "ansible", "bootstrap")
  362. if resp.StatusCode != daemon.REQUEST_OK {
  363. return &DaemonClientError{SockMsg: resp}
  364. }
  365. return nil
  366. }
  367. /*
  368. Force the daemon to reload its configuration
  369. */
  370. func (d DaemonClient) ForceReload() error {
  371. resp := d.Call([]byte(BLANK_JSON), "config", "reload")
  372. if resp.StatusCode != daemon.REQUEST_OK {
  373. return &DaemonClientError{SockMsg: resp}
  374. }
  375. return nil
  376. }
  377. /*
  378. Force a configuration save to the daemon/server
  379. */
  380. func (d DaemonClient) ForceSave() error {
  381. resp := d.Call([]byte(BLANK_JSON), "config", "save")
  382. if resp.StatusCode != daemon.REQUEST_OK {
  383. return &DaemonClientError{SockMsg: resp}
  384. }
  385. return nil
  386. }
  387. /*
  388. This creates a new server, wrapping the DaemonClient.NewServer() function, and then configures it
  389. :param name: the name to give the server
  390. */
  391. func (d DaemonClient) ServiceInit(name string) error {
  392. d.NewServer(name)
  393. resp := d.Call(jsonBuilder(linode.PollLinodeRequest{}, name), "cloud", "poll")
  394. if resp.StatusCode != daemon.REQUEST_OK {
  395. return &DaemonClientError{SockMsg: resp}
  396. }
  397. resp = d.Call(jsonBuilder(semaphore.SemaphoreRequest{}, "VPN Rotation playbook"), "ansible-job", "run")
  398. if resp.StatusCode != daemon.REQUEST_OK {
  399. return &DaemonClientError{SockMsg: resp}
  400. }
  401. semaphoreResp := semaphore.TaskInfo{}
  402. err := json.Unmarshal(resp.Body, &semaphoreResp)
  403. if err != nil {
  404. return &DaemonClientError{SockMsg: resp}
  405. }
  406. resp = d.Call(jsonBuilder(semaphore.SemaphoreRequest{}, fmt.Sprint(semaphoreResp.ID)), "ansible-job", "poll")
  407. if resp.StatusCode != daemon.REQUEST_OK {
  408. return &DaemonClientError{SockMsg: resp}
  409. }
  410. return nil
  411. }
  412. type DaemonClientError struct {
  413. SockMsg daemon.SockMessage
  414. }
  415. func (d *DaemonClientError) Error() string {
  416. return fmt.Sprintf("Response Code: %v, Response Message: %s, Body: %s", d.SockMsg.StatusCode, d.SockMsg.StatusMsg, string(d.SockMsg.Body))
  417. }
  418. type ServerNotFound struct {
  419. Name string
  420. }
  421. func (s *ServerNotFound) Error() string {
  422. return "Server with name: " + s.Name + " was not found."
  423. }