client.go 13 KB

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