├── .github └── workflows │ └── push.yml ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── accounting.go ├── accounting_fields.go ├── authenticate.go ├── authenticate_fields.go ├── authorize.go ├── authorize_fields.go ├── authorize_fields_test.go ├── client.go ├── cmds ├── client │ ├── authen_ascii.go │ ├── authen_pap.go │ └── main.go └── server │ ├── config │ ├── aaa.go │ ├── accounters │ │ ├── local │ │ │ └── local.go │ │ └── syslog │ │ │ └── syslog.go │ ├── authenticators │ │ ├── bcrypt │ │ │ ├── bcrypt.go │ │ │ └── generator │ │ │ │ └── main.go │ │ └── shared.go │ ├── authorizers │ │ └── stringy │ │ │ ├── authorize_fields_benchmark_test.go │ │ │ ├── cmd_v2_test.go │ │ │ ├── command.go │ │ │ ├── command_v2.go │ │ │ ├── log.go │ │ │ ├── session.go │ │ │ ├── stats.go │ │ │ ├── stringy.go │ │ │ ├── stringy_test.go │ │ │ ├── test │ │ │ ├── log.go │ │ │ └── stringy_test.go │ │ │ └── tokenized_commands_test.go │ ├── provider.go │ ├── secret │ │ ├── dns │ │ │ ├── provider.go │ │ │ └── stats.go │ │ ├── keychain.go │ │ └── prefix │ │ │ └── provider.go │ └── types.go │ ├── exporter │ └── exporter.go │ ├── handlers │ ├── acct.go │ ├── authen.go │ ├── authen_ascii.go │ ├── authen_pap.go │ ├── author.go │ ├── config.go │ ├── log.go │ ├── response_logger.go │ ├── span.go │ ├── start.go │ └── stats.go │ ├── loader │ ├── fsnotify │ │ └── fsnotify.go │ ├── json │ │ └── json.go │ ├── loader.go │ ├── prefix_filter.go │ ├── prefixfilter_test.go │ ├── stats.go │ ├── testdata │ │ └── test_config.yaml │ ├── yaml │ │ └── yaml.go │ └── yaml_test.go │ ├── log │ ├── log.go │ └── log_test.go │ ├── main.go │ ├── support.go │ ├── tacquito.yaml │ └── test │ ├── acct_test.go │ ├── authen_mocks.go │ ├── authen_test.go │ ├── author_test.go │ ├── config_yaml.go │ ├── mocks.go │ ├── server_bench_test.go │ ├── smash_test.go │ └── testdata │ └── test_config.yaml ├── crypt.go ├── crypt_test.go ├── ctx_keys.go ├── docs └── imgs │ ├── service_overview.jpg │ └── tacquito-mascot.png ├── go.mod ├── go.sum ├── handlers.go ├── header.go ├── header_fields.go ├── header_fields_test.go ├── log.go ├── packet.go ├── packet_test.go ├── proxy ├── readerwriter.go └── readerwriter_test.go ├── request_fields_test.go ├── secret.go ├── server.go ├── server_test.go ├── sessions.go └── stats.go /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: tq push 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Set up Go 13 | uses: actions/setup-go@v3 14 | with: 15 | go-version: 1.23 16 | 17 | - name: Build 18 | run: go build -v ./... 19 | 20 | - name: Test 21 | run: go test -v ./... 22 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Surya Ahuja (Production Engineer) 2 | * Chris Gorham (Production Engineer) 3 | * Joe Hrbek (Production Engineer) 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to tacquito 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `main`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 5. Make sure your code lints. 13 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 14 | 15 | ## Contributor License Agreement ("CLA") 16 | In order to accept your pull request, we need you to submit a CLA. You only need 17 | to do this once to work on any of Facebook's open source projects. 18 | 19 | Complete your CLA here: 20 | 21 | ## Issues 22 | We use GitHub issues to track public bugs. Please ensure your description is 23 | clear and has sufficient instructions to be able to reproduce the issue. When 24 | troubleshooting packet issues, a pcap is very helpful. 25 | 26 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 27 | disclosure of security bugs. In those cases, please go through the process 28 | outlined on that page and do not file a public issue. 29 | 30 | ## License 31 | By contributing to tacquito, you agree that your contributions will be licensed 32 | under the LICENSE file in the root directory of this source tree. 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /authorize_fields_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | func TestArgsStripCR(t *testing.T) { 15 | tests := []Args{ 16 | { 17 | "cmd=show", 18 | "cmd-arg=version", 19 | "cmd-arg=", 20 | }, 21 | { 22 | "cmd=show", 23 | "cmd-arg=version", 24 | "cmd-arg=", 25 | }, 26 | } 27 | expected := "version" 28 | for _, args := range tests { 29 | if v := args.CommandArgsNoLE(); v != expected { 30 | t.Fatalf("failed to get command args, expected %s, got %s", expected, v) 31 | } 32 | } 33 | 34 | } 35 | 36 | func TestArgsStripCRInMiddle(t *testing.T) { 37 | args := Args{ 38 | "cmd=show", 39 | "cmd-arg=version", 40 | "cmd-arg=", 41 | "cmd-arg=actual", 42 | "cmd-arg=line", 43 | "cmd-arg=ending", 44 | "cmd-arg=", 45 | } 46 | expected := "version actual line ending" 47 | if v := args.CommandArgsNoLE(); v != expected { 48 | t.Fatalf("failed to get command args, expected %s, got %s", expected, v) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "fmt" 12 | "net" 13 | ) 14 | 15 | // ClientOption is a setter type for Client 16 | type ClientOption func(c *Client) error 17 | 18 | // SetClientDialer see net.ResolveTCPAddr for details, this follows 19 | // the same input requirements for network and address. It will then use net.DialTCP 20 | // with a nil source addr and a constructed TCPAddr from the provided network and address. 21 | // A secret for the connection must also be provided. 22 | func SetClientDialer(network, address string, secret []byte) ClientOption { 23 | return func(c *Client) error { 24 | tcpAddr, err := net.ResolveTCPAddr(network, address) 25 | if err != nil { 26 | return err 27 | } 28 | conn, err := net.DialTCP(network, nil, tcpAddr) 29 | if err != nil { 30 | return err 31 | } 32 | c.crypter = newCrypter(secret, conn, false) 33 | return nil 34 | } 35 | } 36 | 37 | // SetClientWithConn uses an existing connection to create a [ClientOption]. 38 | // This is useful for cases where a custom connection is required. 39 | // A secret for the connection must also be provided. 40 | func SetClientWithConn(conn *net.TCPConn, secret []byte) ClientOption { 41 | return func(c *Client) error { 42 | c.crypter = newCrypter(secret, conn, false) 43 | return nil 44 | } 45 | } 46 | 47 | // SetClientDialerWithLocalAddr see net.ResolveTCPAddr for details, this follows 48 | // the same input requirements for network and address. raddr is the destination tcp address 49 | // to dial to, and laddr is the client address to dial from, if set to an empty string, then 50 | // the function will fall back to DialTCP's default selection of a local interface 51 | // with a nil source addr and a constructed TCPAddr from the provided network and address. 52 | // A secret for the connection must also be provided. 53 | func SetClientDialerWithLocalAddr(network, raddr, laddr string, secret []byte) ClientOption { 54 | return func(c *Client) error { 55 | localAddr, err := net.ResolveTCPAddr(network, laddr) 56 | if err != nil { 57 | fmt.Printf("unable to assign local address %v:%v, a default address will be chosen", laddr, err) 58 | } 59 | tcpAddr, err := net.ResolveTCPAddr(network, raddr) 60 | if err != nil { 61 | return err 62 | } 63 | conn, err := net.DialTCP(network, localAddr, tcpAddr) 64 | if err != nil { 65 | return err 66 | } 67 | c.crypter = newCrypter(secret, conn, false) 68 | return nil 69 | } 70 | } 71 | 72 | // NewClient creates a new client 73 | func NewClient(opts ...ClientOption) (*Client, error) { 74 | c := &Client{} 75 | defaults := []ClientOption{} 76 | opts = append(defaults, opts...) 77 | for _, opt := range opts { 78 | if err := opt(c); err != nil { 79 | return nil, err 80 | } 81 | } 82 | return c, nil 83 | } 84 | 85 | // Client base client implementation for server/client communication 86 | type Client struct { 87 | crypter *crypter 88 | } 89 | 90 | // Send sends a packet to the server and decodes the response. If multiple packet exchanges are 91 | // necessary, the caller will need to call this method repeatedly to achieve the desired 92 | // result. 93 | func (c *Client) Send(p *Packet) (*Packet, error) { 94 | _, err := c.crypter.write(p) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return c.crypter.read() 99 | 100 | } 101 | 102 | // SendOnly sends a packet to the server. It does not decode the response. 103 | func (c *Client) SendOnly(p *Packet) error { 104 | _, err := c.crypter.write(p) 105 | if err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | // Close ... 112 | func (c *Client) Close() error { 113 | return c.crypter.Close() 114 | } 115 | -------------------------------------------------------------------------------- /cmds/client/authen_ascii.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package main provides a basic tacacs test client for use with tacacs servers and tacquito 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "os" 14 | 15 | tq "github.com/facebookincubator/tacquito" 16 | ) 17 | 18 | type asciiSequence struct { 19 | packet *tq.Packet 20 | validate func(response []byte) 21 | } 22 | 23 | func ascii(c *tq.Client) { 24 | fmt.Println("execute ascii authentication") 25 | var resp *tq.Packet 26 | var err error 27 | for _, s := range newASCIIAuthenSequence(getPassword()) { 28 | resp, err = c.Send(s.packet) 29 | if err != nil { 30 | fmt.Printf("%v\n", err) 31 | os.Exit(1) 32 | } 33 | s.validate(resp.Body) 34 | } 35 | printASCIIResponse(resp) 36 | } 37 | func newASCIIAuthenSequence(password string) []asciiSequence { 38 | authenRequest := asciiSequence{ 39 | packet: tq.NewPacket( 40 | tq.SetPacketHeader( 41 | tq.NewHeader( 42 | tq.SetHeaderVersion(tq.Version{MajorVersion: tq.MajorVersion, MinorVersion: tq.MinorVersionDefault}), 43 | tq.SetHeaderType(tq.Authenticate), 44 | tq.SetHeaderSessionID(12345), 45 | ), 46 | ), 47 | tq.SetPacketBodyUnsafe( 48 | tq.NewAuthenStart( 49 | tq.SetAuthenStartAction(tq.AuthenActionLogin), 50 | tq.SetAuthenStartPrivLvl(tq.PrivLvlUser), 51 | tq.SetAuthenStartType(tq.AuthenTypeASCII), 52 | tq.SetAuthenStartService(tq.AuthenServiceLogin), 53 | tq.SetAuthenStartPort(tq.AuthenPort("tty0")), 54 | tq.SetAuthenStartRemAddr(tq.AuthenRemAddr("devvm2515")), 55 | ), 56 | ), 57 | ), 58 | validate: func(response []byte) { 59 | var body tq.AuthenReply 60 | if err := tq.Unmarshal(response, &body); err != nil { 61 | fmt.Printf("%v", err) 62 | os.Exit(1) 63 | } 64 | if body.Status != tq.AuthenStatusGetUser { 65 | fmt.Println("failed to match AuthenStatusGetUser") 66 | os.Exit(1) 67 | } 68 | }, 69 | } 70 | 71 | authenContinueUsername := asciiSequence{ 72 | packet: tq.NewPacket( 73 | tq.SetPacketHeader( 74 | tq.NewHeader( 75 | tq.SetHeaderVersion(tq.Version{MajorVersion: tq.MajorVersion, MinorVersion: tq.MinorVersionDefault}), 76 | tq.SetHeaderType(tq.Authenticate), 77 | tq.SetHeaderSeqNo(3), 78 | tq.SetHeaderSessionID(12345), 79 | ), 80 | ), 81 | tq.SetPacketBodyUnsafe( 82 | tq.NewAuthenContinue( 83 | tq.SetAuthenContinueUserMessage(tq.AuthenUserMessage(*username)), 84 | ), 85 | ), 86 | ), 87 | validate: func(response []byte) { 88 | var body tq.AuthenReply 89 | if err := tq.Unmarshal(response, &body); err != nil { 90 | fmt.Printf("%v", err) 91 | os.Exit(1) 92 | } 93 | if body.Status != tq.AuthenStatusGetPass { 94 | fmt.Println("failed to match AuthenStatusGetPass") 95 | os.Exit(1) 96 | } 97 | }, 98 | } 99 | 100 | authenContinuePassword := asciiSequence{ 101 | packet: tq.NewPacket( 102 | tq.SetPacketHeader( 103 | tq.NewHeader( 104 | tq.SetHeaderVersion(tq.Version{MajorVersion: tq.MajorVersion, MinorVersion: tq.MinorVersionDefault}), 105 | tq.SetHeaderType(tq.Authenticate), 106 | tq.SetHeaderSeqNo(5), 107 | tq.SetHeaderSessionID(12345), 108 | ), 109 | ), 110 | tq.SetPacketBodyUnsafe( 111 | tq.NewAuthenContinue( 112 | tq.SetAuthenContinueUserMessage(tq.AuthenUserMessage(password)), 113 | ), 114 | ), 115 | ), 116 | validate: func(response []byte) { 117 | var body tq.AuthenReply 118 | if err := tq.Unmarshal(response, &body); err != nil { 119 | fmt.Printf("%v", err) 120 | os.Exit(1) 121 | } 122 | if body.Status != tq.AuthenStatusPass { 123 | fmt.Println("failed to match AuthenStatusPass") 124 | os.Exit(1) 125 | } 126 | }, 127 | } 128 | return []asciiSequence{ 129 | authenRequest, 130 | authenContinueUsername, 131 | authenContinuePassword, 132 | } 133 | } 134 | 135 | func printASCIIResponse(resp *tq.Packet) { 136 | var body tq.AuthenReply 137 | if err := tq.Unmarshal(resp.Body, &body); err != nil { 138 | fmt.Printf("\n%v\n", err) 139 | os.Exit(1) 140 | } 141 | fmt.Printf("\n%+v\n", body) 142 | } 143 | -------------------------------------------------------------------------------- /cmds/client/authen_pap.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package main provides a basic tacacs test client for use with tacacs servers and tacquito 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "os" 14 | 15 | tq "github.com/facebookincubator/tacquito" 16 | ) 17 | 18 | func pap(c *tq.Client) { 19 | fmt.Println("execute pap authentication") 20 | req := newPAPRequest(getPassword()) 21 | resp, err := c.Send(req) 22 | if err != nil { 23 | fmt.Printf("%v\n", err) 24 | os.Exit(1) 25 | } 26 | printPAPResponse(resp) 27 | } 28 | 29 | func newPAPRequest(password string) *tq.Packet { 30 | return tq.NewPacket( 31 | tq.SetPacketHeader( 32 | tq.NewHeader( 33 | tq.SetHeaderVersion(tq.Version{MajorVersion: tq.MajorVersion, MinorVersion: tq.MinorVersionOne}), 34 | tq.SetHeaderType(tq.Authenticate), 35 | tq.SetHeaderRandomSessionID(), 36 | ), 37 | ), 38 | tq.SetPacketBodyUnsafe( 39 | tq.NewAuthenStart( 40 | tq.SetAuthenStartType(tq.AuthenTypePAP), 41 | tq.SetAuthenStartAction(tq.AuthenActionLogin), 42 | tq.SetAuthenStartPrivLvl(tq.PrivLvl(*privLvl)), 43 | tq.SetAuthenStartPort(tq.AuthenPort(*port)), 44 | tq.SetAuthenStartRemAddr(tq.AuthenRemAddr(*remAddr)), 45 | tq.SetAuthenStartUser(tq.AuthenUser(*username)), 46 | tq.SetAuthenStartData(tq.AuthenData(password)), 47 | ), 48 | ), 49 | ) 50 | } 51 | 52 | func printPAPResponse(resp *tq.Packet) { 53 | var body tq.AuthenReply 54 | if err := tq.Unmarshal(resp.Body, &body); err != nil { 55 | fmt.Printf("\n%v\n", err) 56 | os.Exit(1) 57 | } 58 | fmt.Printf("\n%+v\n", body) 59 | } 60 | -------------------------------------------------------------------------------- /cmds/client/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package main provides a basic tacacs test client for use with tacacs servers and tacquito 9 | package main 10 | 11 | import ( 12 | "flag" 13 | "fmt" 14 | "os" 15 | 16 | tq "github.com/facebookincubator/tacquito" 17 | 18 | "golang.org/x/term" 19 | ) 20 | 21 | var ( 22 | username = flag.String("username", "", "the username to use when authenticating.") 23 | password = flag.String("password", "", "the password to use when authenticating.") 24 | privLvl = flag.Int("priv-lvl", 1, "the priv lvl that the client is requesting to auth with.") 25 | network = flag.String("network", "tcp6", "listen on tcp or tcp6") 26 | address = flag.String("address", ":2046", "listen on the provided address:port") 27 | port = flag.String("port", "", "the port the client is sourced from, tty0 for example.") 28 | remAddr = flag.String("rem-addr", "", "the remote address the client is coming from.") 29 | secret = flag.String("secret", "fooman", "the tacacs secret to be used.") 30 | authenMode = flag.String("authen-mode", "pap", "valid choices, [pap ascii]") 31 | ) 32 | 33 | func main() { 34 | flag.Parse() 35 | verifyFlags() 36 | 37 | c, err := tq.NewClient(tq.SetClientDialer(*network, *address, []byte(*secret))) 38 | if err != nil { 39 | fmt.Printf("%v\n", err) 40 | os.Exit(1) 41 | } 42 | defer c.Close() 43 | switch *authenMode { 44 | case "pap": 45 | pap(c) 46 | case "ascii": 47 | ascii(c) 48 | default: 49 | fmt.Printf("%v is an invalid mode", *authenMode) 50 | } 51 | } 52 | 53 | func verifyFlags() { 54 | if *username == "" { 55 | fmt.Println("invalid username, please provide one") 56 | os.Exit(1) 57 | } 58 | if *secret == "" { 59 | fmt.Println("invalid secret, you must provide one") 60 | os.Exit(1) 61 | } 62 | } 63 | 64 | func getPassword() string { 65 | if *password != "" { 66 | return *password 67 | } 68 | fmt.Print("Enter Password: ") 69 | raw, err := term.ReadPassword(0) 70 | if err != nil { 71 | fmt.Println("unable to read password") 72 | os.Exit(1) 73 | } 74 | return string(raw) 75 | } 76 | -------------------------------------------------------------------------------- /cmds/server/config/aaa.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package config 9 | 10 | import ( 11 | tq "github.com/facebookincubator/tacquito" 12 | ) 13 | 14 | // loggerProvider provides the logging implementation 15 | type loggerProvider interface { 16 | Infof(format string, args ...interface{}) 17 | Errorf(format string, args ...interface{}) 18 | } 19 | 20 | // AAAOption ... 21 | type AAAOption func(a *AAA) 22 | 23 | // SetAAALogger sets the logging backend 24 | func SetAAALogger(l loggerProvider) AAAOption { 25 | return func(a *AAA) { 26 | a.loggerProvider = l 27 | } 28 | } 29 | 30 | // SetAAAUser creats a scoped config for user 31 | func SetAAAUser(u User) AAAOption { 32 | return func(a *AAA) { 33 | a.User = u 34 | } 35 | } 36 | 37 | // SetAAAAuthenticator sets the authenticator 38 | func SetAAAAuthenticator(h tq.Handler) AAAOption { 39 | return func(a *AAA) { 40 | a.Authenticate = h 41 | } 42 | } 43 | 44 | // SetAAAAuthorizer sets the authorizer 45 | func SetAAAAuthorizer(h tq.Handler) AAAOption { 46 | return func(a *AAA) { 47 | a.Authorizer = h 48 | } 49 | } 50 | 51 | // SetAAAAccounter sets the accounter 52 | func SetAAAAccounter(h tq.Handler) AAAOption { 53 | return func(a *AAA) { 54 | a.Accounting = h 55 | } 56 | } 57 | 58 | // NewAAA creates a user scope aaa handler grouping 59 | func NewAAA(opts ...AAAOption) *AAA { 60 | a := &AAA{ 61 | Authenticate: &defaultAuthenticator{}, 62 | Authorizer: &defaultAuthorizer{}, 63 | Accounting: &defaultAccounter{}, 64 | } 65 | 66 | for _, opt := range opts { 67 | opt(a) 68 | } 69 | return a 70 | } 71 | 72 | // AAA is a user level aaa handler grouping that will provide default behaviors 73 | // for each user if the corresponding A is not injected during loader runs 74 | type AAA struct { 75 | User 76 | loggerProvider 77 | Authenticate tq.Handler 78 | Authorizer tq.Handler 79 | Accounting tq.Handler 80 | } 81 | 82 | type defaultAuthenticator struct{} 83 | 84 | // Authenticate default deny implementation 85 | func (a *defaultAuthenticator) Handle(response tq.Response, request tq.Request) { 86 | response.Reply( 87 | tq.NewAuthenReply( 88 | tq.SetAuthenReplyStatus(tq.AuthenStatusFail), 89 | tq.SetAuthenReplyServerMsg("authentication denied"), 90 | ), 91 | ) 92 | } 93 | 94 | type defaultAuthorizer struct{} 95 | 96 | // Authorize default deny implementation 97 | func (a *defaultAuthorizer) Handle(response tq.Response, request tq.Request) { 98 | response.Reply( 99 | tq.NewAuthorReply( 100 | tq.SetAuthorReplyStatus(tq.AuthorStatusFail), 101 | tq.SetAuthorReplyServerMsg("authorization denied"), 102 | ), 103 | ) 104 | } 105 | 106 | type defaultAccounter struct{} 107 | 108 | // Accounting default deny implementation 109 | func (a *defaultAccounter) Handle(response tq.Response, request tq.Request) { 110 | response.Reply( 111 | tq.NewAcctReply( 112 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 113 | tq.SetAcctReplyServerMsg("accounting denied"), 114 | ), 115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /cmds/server/config/accounters/local/local.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package local supports writing Accounting logs to the local system via a log.Logger 9 | package local 10 | 11 | import ( 12 | "context" 13 | "encoding/json" 14 | "fmt" 15 | "log" 16 | "os" 17 | 18 | tq "github.com/facebookincubator/tacquito" 19 | ) 20 | 21 | // loggerProvider provides the logging implementation for local server events 22 | type loggerProvider interface { 23 | Infof(ctx context.Context, format string, args ...interface{}) 24 | Errorf(ctx context.Context, format string, args ...interface{}) 25 | } 26 | 27 | // our log.Logger interface 28 | type acctLogger interface { 29 | Printf(format string, args ...interface{}) 30 | } 31 | 32 | // Option is the setter type for Accounter 33 | type Option func(a *Accounter) 34 | 35 | // SetLogSinkDefault will create a file object for writing logs to and attach it to the accounting logger 36 | func SetLogSinkDefault(path, prefix string) Option { 37 | return func(a *Accounter) { 38 | // open file for accounting data 39 | f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 40 | if err != nil { 41 | return 42 | } 43 | a.sink = log.New(f, prefix, log.Ldate|log.Ltime|log.Llongfile) 44 | } 45 | } 46 | 47 | // SetLogSink will use the acctLogger interface to create a local logger 48 | func SetLogSink(l acctLogger) Option { 49 | return func(a *Accounter) { 50 | a.sink = l 51 | } 52 | } 53 | 54 | // Accounter that writes to system log service 55 | type Accounter struct { 56 | loggerProvider // local server event logger 57 | sink acctLogger // accounting log destination 58 | } 59 | 60 | // New creates a new accounter. 61 | // TODO: Implement log rotation 62 | func New(l loggerProvider, opts ...Option) (*Accounter, error) { 63 | a := &Accounter{loggerProvider: l} 64 | for _, opt := range opts { 65 | opt(a) 66 | } 67 | if a.sink == nil { 68 | return nil, fmt.Errorf("a log backend is required, please call SetLogSinkDefault or SetLogSink") 69 | } 70 | return a, nil 71 | } 72 | 73 | // New creates a new local file accounter 74 | func (a Accounter) New(options map[string]string) tq.Handler { 75 | return &Accounter{loggerProvider: a.loggerProvider, sink: a.sink} 76 | } 77 | 78 | // Handle ... 79 | func (a Accounter) Handle(response tq.Response, request tq.Request) { 80 | var body tq.AcctRequest 81 | if err := tq.Unmarshal(request.Body, &body); err != nil { 82 | response.Reply( 83 | tq.NewAcctReply( 84 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 85 | tq.SetAcctReplyServerMsg("accounting failure"), 86 | ), 87 | ) 88 | return 89 | } 90 | 91 | jsonLog, err := json.Marshal(body) 92 | if err != nil { 93 | response.Reply( 94 | tq.NewAcctReply( 95 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 96 | tq.SetAcctReplyServerMsg("failed to log accounting message"), 97 | ), 98 | ) 99 | a.Errorf(request.Context, "failed to write to accounting logger: %v", err) 100 | return 101 | 102 | } 103 | 104 | // log accounting data 105 | a.sink.Printf(string(jsonLog)) 106 | 107 | // start/stop/watchdog don't actually log anything, this is up to you 108 | switch body.Flags { 109 | case tq.AcctFlagStart: 110 | response.Reply( 111 | tq.NewAcctReply( 112 | tq.SetAcctReplyStatus(tq.AcctReplyStatusSuccess), 113 | tq.SetAcctReplyServerMsg("success, logging started"), 114 | ), 115 | ) 116 | return 117 | case tq.AcctFlagStop: 118 | response.Reply( 119 | tq.NewAcctReply( 120 | tq.SetAcctReplyStatus(tq.AcctReplyStatusSuccess), 121 | tq.SetAcctReplyServerMsg("success, logging stopped"), 122 | ), 123 | ) 124 | return 125 | case tq.AcctFlagWatchdog: 126 | if int(request.Header.SeqNo) != 1 { 127 | // cannot be seqno > 1 128 | response.Reply( 129 | tq.NewAcctReply( 130 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 131 | tq.SetAcctReplyServerMsg("invalid sequence number"), 132 | ), 133 | ) 134 | return 135 | } 136 | response.Reply( 137 | tq.NewAcctReply( 138 | tq.SetAcctReplyStatus(tq.AcctReplyStatusSuccess), 139 | tq.SetAcctReplyServerMsg("success, watchdog"), 140 | ), 141 | ) 142 | return 143 | case tq.AcctFlagWatchdogWithUpdate: 144 | if int(request.Header.SeqNo) < 3 { 145 | // cannot be seqno 1 or 2 146 | response.Reply( 147 | tq.NewAcctReply( 148 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 149 | tq.SetAcctReplyServerMsg("invalid sequence number"), 150 | ), 151 | ) 152 | return 153 | } 154 | response.Reply( 155 | tq.NewAcctReply( 156 | tq.SetAcctReplyStatus(tq.AcctReplyStatusSuccess), 157 | tq.SetAcctReplyServerMsg("success, watchdog update"), 158 | ), 159 | ) 160 | return 161 | } 162 | response.Reply( 163 | tq.NewAcctReply( 164 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 165 | tq.SetAcctReplyServerMsg("unexpected accounting flag"), 166 | ), 167 | ) 168 | } 169 | -------------------------------------------------------------------------------- /cmds/server/config/accounters/syslog/syslog.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package syslog supports ending Accounting data in JSON format to syslog 9 | // Windows is unsupported 10 | package syslog 11 | 12 | import ( 13 | "encoding/json" 14 | "log/syslog" 15 | 16 | tq "github.com/facebookincubator/tacquito" 17 | ) 18 | 19 | // loggerProvider provides the logging implementation for local server events 20 | type loggerProvider interface { 21 | Infof(format string, args ...interface{}) 22 | Errorf(format string, args ...interface{}) 23 | } 24 | 25 | // Accounter that writes to system log service 26 | type Accounter struct { 27 | loggerProvider // local server event logger 28 | *syslog.Writer // syslog writer 29 | } 30 | 31 | // New ... 32 | func New(l loggerProvider, writer *syslog.Writer) *Accounter { 33 | return &Accounter{loggerProvider: l, Writer: writer} 34 | } 35 | 36 | // New creates a new syslog accounter 37 | func (a Accounter) New(options map[string]string) tq.Handler { 38 | return &Accounter{loggerProvider: a.loggerProvider, Writer: a.Writer} 39 | } 40 | 41 | // Handle ... 42 | func (a Accounter) Handle(response tq.Response, request tq.Request) { 43 | var body tq.AcctRequest 44 | if err := tq.Unmarshal(request.Body, &body); err != nil { 45 | response.Reply( 46 | tq.NewAcctReply( 47 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 48 | tq.SetAcctReplyServerMsg("accounting failure"), 49 | ), 50 | ) 51 | return 52 | } 53 | 54 | jsonLog, err := json.Marshal(body) 55 | if err != nil { 56 | response.Reply( 57 | tq.NewAcctReply( 58 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 59 | tq.SetAcctReplyServerMsg("failed to log accounting message"), 60 | ), 61 | ) 62 | a.Errorf("failed marshal accounting log: %v", err) 63 | return 64 | 65 | } 66 | 67 | // log accounting data 68 | if _, err := a.Write(jsonLog); err != nil { 69 | response.Reply( 70 | tq.NewAcctReply( 71 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 72 | tq.SetAcctReplyServerMsg("failed to log accounting message"), 73 | ), 74 | ) 75 | a.Errorf("failed to write accounting data to syslog: %v", err) 76 | return 77 | 78 | } 79 | 80 | // start/stop/watchdog don't actually log anything, this is up to you 81 | switch body.Flags { 82 | case tq.AcctFlagStart: 83 | response.Reply( 84 | tq.NewAcctReply( 85 | tq.SetAcctReplyStatus(tq.AcctReplyStatusSuccess), 86 | tq.SetAcctReplyServerMsg("success, logging started"), 87 | ), 88 | ) 89 | return 90 | case tq.AcctFlagStop: 91 | response.Reply( 92 | tq.NewAcctReply( 93 | tq.SetAcctReplyStatus(tq.AcctReplyStatusSuccess), 94 | tq.SetAcctReplyServerMsg("success, logging stopped"), 95 | ), 96 | ) 97 | return 98 | case tq.AcctFlagWatchdog: 99 | if int(request.Header.SeqNo) != 1 { 100 | // cannot be seqno > 1 101 | response.Reply( 102 | tq.NewAcctReply( 103 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 104 | tq.SetAcctReplyServerMsg("invalid sequence number"), 105 | ), 106 | ) 107 | return 108 | } 109 | response.Reply( 110 | tq.NewAcctReply( 111 | tq.SetAcctReplyStatus(tq.AcctReplyStatusSuccess), 112 | tq.SetAcctReplyServerMsg("success, watchdog"), 113 | ), 114 | ) 115 | return 116 | case tq.AcctFlagWatchdogWithUpdate: 117 | if int(request.Header.SeqNo) < 3 { 118 | // cannot be seqno 1 or 2 119 | response.Reply( 120 | tq.NewAcctReply( 121 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 122 | tq.SetAcctReplyServerMsg("invalid sequence number"), 123 | ), 124 | ) 125 | return 126 | } 127 | response.Reply( 128 | tq.NewAcctReply( 129 | tq.SetAcctReplyStatus(tq.AcctReplyStatusSuccess), 130 | tq.SetAcctReplyServerMsg("success, watchdog update"), 131 | ), 132 | ) 133 | return 134 | } 135 | response.Reply( 136 | tq.NewAcctReply( 137 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 138 | tq.SetAcctReplyServerMsg("unexpected accounting flag"), 139 | ), 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /cmds/server/config/authenticators/bcrypt/bcrypt.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package bcrypt implements a tqcquito Config interface. It uses bcrypt 9 | // to secure the password in a hashed form and stores it statically in source. 10 | // This is strictly an example of how this interface might be implemented. It is 11 | // not recommended to be used in production and is only an example. 12 | package bcrypt 13 | 14 | import ( 15 | "context" 16 | "encoding/hex" 17 | "fmt" 18 | 19 | tq "github.com/facebookincubator/tacquito" 20 | "github.com/facebookincubator/tacquito/cmds/server/config/authenticators" 21 | 22 | "golang.org/x/crypto/bcrypt" 23 | ) 24 | 25 | // loggerProvider provides the logging implementation 26 | type loggerProvider interface { 27 | Infof(ctx context.Context, format string, args ...interface{}) 28 | Errorf(ctx context.Context, format string, args ...interface{}) 29 | Record(ctx context.Context, r map[string]string, obscure ...string) 30 | } 31 | 32 | // getSecret is the expected behavior for fetching sha hashes from keychain 33 | // types that implement this should be thread safe 34 | type getSecret interface { 35 | GetSecret(ctx context.Context, name, group string) ([]byte, error) 36 | } 37 | 38 | // supportedOptions map will be unmarshaled into this type 39 | // 40 | // hash - if present, we use it blindly until a config change removes it. 41 | // group - the group that holds the key we're looking for 42 | // key - the key in the keychain group. this is may or may not be == username 43 | func newSupportedOptions(username string, options map[string]string) supportedOptions { 44 | opts := supportedOptions{ 45 | hash: options["hash"], 46 | group: options["group"], 47 | key: options["key"], 48 | } 49 | if opts.key == "" { 50 | opts.key = username 51 | } 52 | return opts 53 | } 54 | 55 | type supportedOptions struct { 56 | // hash - if present, we use it blindly until a config change removes it. Hash is optional. 57 | hash string 58 | // group - the group within keychain that holds the key we're looking for. group is optional 59 | group string 60 | // key - the key in the group within keychain. this is may or may not be == username 61 | key string 62 | } 63 | 64 | func (s *supportedOptions) setKey(username string) { 65 | if s.key == "" { 66 | s.key = username 67 | } 68 | } 69 | 70 | func (s supportedOptions) validate() error { 71 | if len(s.hash) == 0 && len(s.key) == 0 { 72 | return fmt.Errorf("missing required option keys for bcrypt authenticator; %v", s) 73 | } 74 | return nil 75 | } 76 | 77 | // New Bcrypt Authenticator 78 | func New(l loggerProvider, s getSecret) *Authenticator { 79 | return &Authenticator{loggerProvider: l} 80 | } 81 | 82 | // Authenticator with bcrypt password hashing used for validation 83 | type Authenticator struct { 84 | loggerProvider 85 | authenticators.Methods 86 | username string 87 | supportedOptions 88 | 89 | getSecret 90 | } 91 | 92 | // New creates a new bcrypt authenticator which implements tq.Config 93 | func (a Authenticator) New(username string, options map[string]string) (tq.Handler, error) { 94 | opts := newSupportedOptions(username, options) 95 | if err := opts.validate(); err != nil { 96 | return nil, err 97 | } 98 | return &Authenticator{loggerProvider: a.loggerProvider, username: username, supportedOptions: opts}, nil 99 | } 100 | 101 | // Handle handles all authenticate message types, scoped to the uid 102 | func (a Authenticator) Handle(response tq.Response, request tq.Request) { 103 | password, err := a.GetPassword(request) 104 | if err != nil { 105 | response.Reply( 106 | tq.NewAuthenReply( 107 | tq.SetAuthenReplyStatus(tq.AuthenStatusError), 108 | tq.SetAuthenReplyServerMsg(fmt.Sprintf("%v", err)), 109 | ), 110 | ) 111 | return 112 | } 113 | var expectedHash []byte 114 | if len(a.hash) > 0 { 115 | // if hash was a key in options, we see that as an override and do not call keychain 116 | secret, err := hex.DecodeString(a.hash) 117 | if err != nil { 118 | a.Errorf(request.Context, "error decoding the hex encoded password for user [%v]; %v", a.username, err) 119 | response.Reply( 120 | tq.NewAuthenReply( 121 | tq.SetAuthenReplyStatus(tq.AuthenStatusFail), 122 | tq.SetAuthenReplyServerMsg("login failure"), 123 | ), 124 | ) 125 | return 126 | } 127 | expectedHash = secret 128 | } else { 129 | secret, err := a.GetSecret(request.Context, a.username, a.group) 130 | if err != nil { 131 | a.Errorf(request.Context, "failure in keychain query for user [%v] using a sha512 hashed password; %v", a.username, err) 132 | response.Reply( 133 | tq.NewAuthenReply( 134 | tq.SetAuthenReplyStatus(tq.AuthenStatusFail), 135 | tq.SetAuthenReplyServerMsg("login failure"), 136 | ), 137 | ) 138 | } 139 | expectedHash = secret 140 | } 141 | 142 | if err := bcrypt.CompareHashAndPassword(expectedHash, []byte(password)); err == nil { 143 | a.Infof(request.Context, "accepting user [%v] using a bcrypt password", a.username) 144 | response.Reply( 145 | tq.NewAuthenReply( 146 | tq.SetAuthenReplyStatus(tq.AuthenStatusPass), 147 | ), 148 | ) 149 | return 150 | } 151 | 152 | a.Errorf(request.Context, "failed to validate the user [%v] using a bcrypt password", a.username) 153 | response.Reply( 154 | tq.NewAuthenReply( 155 | tq.SetAuthenReplyStatus(tq.AuthenStatusFail), 156 | tq.SetAuthenReplyServerMsg("login failure"), 157 | ), 158 | ) 159 | } 160 | -------------------------------------------------------------------------------- /cmds/server/config/authenticators/bcrypt/generator/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package main provides a utility to create or verify bcrypt strings used by the bcrypt authenticator 9 | package main 10 | 11 | import ( 12 | "encoding/hex" 13 | "flag" 14 | "fmt" 15 | "os" 16 | 17 | "golang.org/x/crypto/bcrypt" 18 | "golang.org/x/term" 19 | ) 20 | 21 | var ( 22 | mode = flag.String("mode", "", "supported password hashing modes: [bcrypt, verify-bcrypt]") 23 | ) 24 | 25 | func main() { 26 | flag.Parse() 27 | verifyFlags() 28 | switch *mode { 29 | case "bcrypt": 30 | password := getPassword("Enter Password (echo is off): ") 31 | hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 32 | if err != nil { 33 | fmt.Printf("hash generation failed: %v\n", err) 34 | os.Exit(1) 35 | } 36 | fmt.Println("bcrypt hex value:", hex.EncodeToString(hash)) 37 | case "verify-bcrypt": 38 | password := getPassword("Enter Password (echo is off): ") 39 | hexpw := getPassword("Enter hex value (echo is off): ") 40 | hash, err := hex.DecodeString(hexpw) 41 | if err != nil { 42 | fmt.Printf("hash decode from hex failed: %v\n", err) 43 | os.Exit(1) 44 | } 45 | if err := bcrypt.CompareHashAndPassword(hash, []byte(password)); err != nil { 46 | fmt.Printf("password validation failed: %v\n", err) 47 | os.Exit(1) 48 | } 49 | fmt.Println("password validation success") 50 | default: 51 | fmt.Printf("unknown mode [%v]\n", *mode) 52 | } 53 | } 54 | 55 | func verifyFlags() { 56 | if *mode == "" { 57 | fmt.Println("supported password hashing modes: [bcrypt, verify-bcrypt], please provide one") 58 | os.Exit(1) 59 | } 60 | } 61 | 62 | func getPassword(msg string) string { 63 | fmt.Println(msg) 64 | raw, err := term.ReadPassword(0) 65 | if err != nil { 66 | fmt.Println("unable to read input") 67 | os.Exit(1) 68 | } 69 | return string(raw) 70 | } 71 | -------------------------------------------------------------------------------- /cmds/server/config/authenticators/shared.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package authenticators provides reusable functions for types interested in implementing 9 | // custom authenticators 10 | package authenticators 11 | 12 | import ( 13 | "fmt" 14 | 15 | tq "github.com/facebookincubator/tacquito" 16 | ) 17 | 18 | // Methods is a stateless, bag of functionality, meant to be composed into 19 | // specific authenticator types to reduce boilerplate 20 | type Methods struct{} 21 | 22 | // GetFields is used in structured logging 23 | func (m Methods) GetFields(request tq.Request) map[string]string { 24 | if body := m.getAuthenStart(request); body != nil { 25 | return body.Fields() 26 | } 27 | if body := m.getAuthenContinue(request); body != nil { 28 | return body.Fields() 29 | } 30 | return nil 31 | } 32 | 33 | // getAuthenStart unmarshalls an authenstart packet 34 | func (m Methods) getAuthenStart(request tq.Request) *tq.AuthenStart { 35 | var body tq.AuthenStart 36 | if err := tq.Unmarshal(request.Body, &body); err != nil { 37 | return nil 38 | } 39 | return &body 40 | } 41 | 42 | // getAuthenContinue unmarshalls an authencontinue packet 43 | func (m Methods) getAuthenContinue(request tq.Request) *tq.AuthenContinue { 44 | var body tq.AuthenContinue 45 | if err := tq.Unmarshal(request.Body, &body); err != nil { 46 | return nil 47 | } 48 | return &body 49 | } 50 | 51 | // GetPassword will get the password from an authenstart or authencontinue packet 52 | func (m Methods) GetPassword(request tq.Request) (string, error) { 53 | if body := m.getAuthenStart(request); body != nil { 54 | return string(body.Data), nil 55 | } 56 | if body := m.getAuthenContinue(request); body != nil { 57 | return string(body.UserMessage), nil 58 | } 59 | return "", fmt.Errorf("missing password") 60 | } 61 | -------------------------------------------------------------------------------- /cmds/server/config/authorizers/stringy/authorize_fields_benchmark_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package stringy 9 | 10 | import ( 11 | "testing" 12 | 13 | tq "github.com/facebookincubator/tacquito" 14 | ) 15 | 16 | // BenchmarkSplitNoDelimiter benchmarks the Split function with a command that has no delimiter 17 | func BenchmarkSplitNoDelimiter(b *testing.B) { 18 | args := tq.Args{ 19 | tq.Arg("service=shell"), 20 | tq.Arg("cmd=show"), 21 | tq.Arg("cmd-arg=version"), 22 | tq.Arg("cmd-arg="), 23 | } 24 | delimiter := "|" 25 | 26 | b.ResetTimer() 27 | for range b.N { 28 | _ = args.Split(delimiter) 29 | } 30 | } 31 | 32 | // BenchmarkSplitOneDelimiter benchmarks the Split function with a command that has one delimiter 33 | func BenchmarkSplitOneDelimiter(b *testing.B) { 34 | args := tq.Args{ 35 | tq.Arg("service=shell"), 36 | tq.Arg("cmd=show"), 37 | tq.Arg("cmd-arg=version"), 38 | tq.Arg("cmd-arg=|"), 39 | tq.Arg("cmd-arg=grep"), 40 | tq.Arg("cmd-arg=version"), 41 | tq.Arg("cmd-arg="), 42 | } 43 | delimiter := "|" 44 | 45 | b.ResetTimer() 46 | for range b.N { 47 | _ = args.Split(delimiter) 48 | } 49 | } 50 | 51 | // BenchmarkSplitMultipleDelimiters benchmarks the Split function with a command that has multiple delimiters 52 | func BenchmarkSplitMultipleDelimiters(b *testing.B) { 53 | args := tq.Args{ 54 | tq.Arg("service=shell"), 55 | tq.Arg("cmd=show"), 56 | tq.Arg("cmd-arg=version"), 57 | tq.Arg("cmd-arg=|"), 58 | tq.Arg("cmd-arg=grep"), 59 | tq.Arg("cmd-arg=version"), 60 | tq.Arg("cmd-arg=|"), 61 | tq.Arg("cmd-arg=wc"), 62 | tq.Arg("cmd-arg=-l"), 63 | tq.Arg("cmd-arg="), 64 | } 65 | delimiter := "|" 66 | 67 | b.ResetTimer() 68 | for range b.N { 69 | _ = args.Split(delimiter) 70 | } 71 | } 72 | 73 | // BenchmarkSplitDifferentDelimiter benchmarks the Split function with a different delimiter 74 | func BenchmarkSplitDifferentDelimiter(b *testing.B) { 75 | args := tq.Args{ 76 | tq.Arg("service=shell"), 77 | tq.Arg("cmd=show"), 78 | tq.Arg("cmd-arg=version"), 79 | tq.Arg("cmd-arg=;"), 80 | tq.Arg("cmd-arg=grep"), 81 | tq.Arg("cmd-arg=version"), 82 | tq.Arg("cmd-arg="), 83 | } 84 | delimiter := ";" 85 | 86 | b.ResetTimer() 87 | for range b.N { 88 | _ = args.Split(delimiter) 89 | } 90 | } 91 | 92 | // BenchmarkSplitMaxDelimiters benchmarks the Split function with the maximum allowed number of delimiters 93 | func BenchmarkSplitMaxDelimiters(b *testing.B) { 94 | // Create a request with MaxSplitCount delimiters (5 pipes) 95 | args := tq.Args{ 96 | tq.Arg("service=shell"), 97 | tq.Arg("cmd=show"), 98 | tq.Arg("cmd-arg=version"), 99 | tq.Arg("cmd-arg=|"), 100 | tq.Arg("cmd-arg=grep"), 101 | tq.Arg("cmd-arg=something"), 102 | tq.Arg("cmd-arg=|"), 103 | tq.Arg("cmd-arg=awk"), 104 | tq.Arg("cmd-arg={print $1}"), 105 | tq.Arg("cmd-arg=|"), 106 | tq.Arg("cmd-arg=sort"), 107 | tq.Arg("cmd-arg=|"), 108 | tq.Arg("cmd-arg=uniq"), 109 | tq.Arg("cmd-arg=|"), // MaxSplitCount 110 | tq.Arg("cmd-arg=wc"), 111 | tq.Arg("cmd-arg=-l"), 112 | tq.Arg("cmd-arg="), 113 | } 114 | delimiter := "|" 115 | 116 | b.ResetTimer() 117 | for range b.N { 118 | _ = args.Split(delimiter) 119 | } 120 | } 121 | 122 | // BenchmarkSplitExceedMaxDelimiters benchmarks the Split function with more than the maximum allowed number of delimiters 123 | func BenchmarkSplitExceedMaxDelimiters(b *testing.B) { 124 | args := tq.Args{ 125 | tq.Arg("service=shell"), 126 | tq.Arg("cmd=show"), 127 | tq.Arg("cmd-arg=version"), 128 | tq.Arg("cmd-arg=|"), 129 | tq.Arg("cmd-arg=grep"), 130 | tq.Arg("cmd-arg=something"), 131 | tq.Arg("cmd-arg=|"), 132 | tq.Arg("cmd-arg=awk"), 133 | tq.Arg("cmd-arg={print $1}"), 134 | tq.Arg("cmd-arg=|"), 135 | tq.Arg("cmd-arg=sort"), 136 | tq.Arg("cmd-arg=|"), 137 | tq.Arg("cmd-arg=uniq"), 138 | tq.Arg("cmd-arg=|"), // MaxSplitCount 139 | tq.Arg("cmd-arg=wc"), 140 | tq.Arg("cmd-arg=-l"), 141 | tq.Arg("cmd-arg=|"), // Exceeds MaxSplitCount 142 | tq.Arg("cmd-arg=cat"), 143 | tq.Arg("cmd-arg="), 144 | } 145 | delimiter := "|" 146 | 147 | b.ResetTimer() 148 | for range b.N { 149 | _ = args.Split(delimiter) 150 | } 151 | } 152 | 153 | // BenchmarkSplitLongCommand benchmarks the Split function with a long command 154 | func BenchmarkSplitLongCommand(b *testing.B) { 155 | args := tq.Args{ 156 | tq.Arg("service=shell"), 157 | tq.Arg("cmd=show"), 158 | tq.Arg("cmd-arg=interfaces"), 159 | tq.Arg("cmd-arg=description"), 160 | tq.Arg("cmd-arg=|"), 161 | tq.Arg("cmd-arg=grep"), 162 | tq.Arg("cmd-arg=-v"), 163 | tq.Arg("cmd-arg=down"), 164 | tq.Arg("cmd-arg=|"), 165 | tq.Arg("cmd-arg=grep"), 166 | tq.Arg("cmd-arg=-v"), 167 | tq.Arg("cmd-arg=admin"), 168 | tq.Arg("cmd-arg=|"), 169 | tq.Arg("cmd-arg=sort"), 170 | tq.Arg("cmd-arg=|"), 171 | tq.Arg("cmd-arg=column"), 172 | tq.Arg("cmd-arg=-t"), 173 | tq.Arg("cmd-arg="), 174 | } 175 | delimiter := "|" 176 | 177 | b.ResetTimer() 178 | for range b.N { 179 | _ = args.Split(delimiter) 180 | } 181 | } 182 | 183 | // BenchmarkSplitConsecutiveDelimiters benchmarks the Split function with consecutive delimiters 184 | func BenchmarkSplitConsecutiveDelimiters(b *testing.B) { 185 | args := tq.Args{ 186 | tq.Arg("service=shell"), 187 | tq.Arg("cmd=show"), 188 | tq.Arg("cmd-arg=version"), 189 | tq.Arg("cmd-arg=|"), 190 | tq.Arg("cmd-arg=|"), 191 | tq.Arg("cmd-arg=|"), 192 | tq.Arg("cmd-arg=grep"), 193 | tq.Arg("cmd-arg=version"), 194 | tq.Arg("cmd-arg="), 195 | } 196 | delimiter := "|" 197 | 198 | b.ResetTimer() 199 | for range b.N { 200 | _ = args.Split(delimiter) 201 | } 202 | } 203 | 204 | // BenchmarkSplitEmptytq.Args benchmarks the Split function with empty tq.Args 205 | func BenchmarkSplitEmptyArgs(b *testing.B) { 206 | args := tq.Args{} 207 | delimiter := "|" 208 | 209 | b.ResetTimer() 210 | for range b.N { 211 | _ = args.Split(delimiter) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /cmds/server/config/authorizers/stringy/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package stringy implements the only authorizer package available in tacquito. 9 | package stringy 10 | 11 | import ( 12 | "context" 13 | "regexp" 14 | 15 | tq "github.com/facebookincubator/tacquito" 16 | "github.com/facebookincubator/tacquito/cmds/server/config" 17 | ) 18 | 19 | const ( 20 | // default anchors for regex expressions embedded in command match attributes 21 | // stored as bytes and strs for matching and concatenation 22 | regexStartByte = '^' 23 | regexEndByte = '$' 24 | regexStartStr = "^" 25 | regexEndStr = "$" 26 | ) 27 | 28 | // NewCommandBasedAuthorizer will return a CommandBasedAuthorizer authorizer. If initial request params 29 | // are not suitable for command based, it returns nil 30 | func NewCommandBasedAuthorizer(ctx context.Context, l loggerProvider, b tq.AuthorRequest, u config.User) *CommandBasedAuthorizer { 31 | // commands are also only evaluated if service == shell 32 | if b.Args.Service() != "shell" { 33 | return nil 34 | } 35 | // cmd= and cmd* are not allowed. the command cmd=show etc must be specified 36 | // cmd*show is also not allowed. Only the madatory separator is considered valid 37 | a, s, v := b.Args.CommandSplit() 38 | if a != "cmd" || s != "=" || v == "" { 39 | return nil 40 | } 41 | return &CommandBasedAuthorizer{ctx: ctx, loggerProvider: l, body: b, user: u} 42 | } 43 | 44 | // CommandBasedAuthorizer provides a command based authorizer which only work under the following 45 | // scenarios: 46 | // 47 | // cmd=show cmd-arg=system cmd-arg= 48 | // cmd=show cmd-arg=system 49 | // cmd=show 50 | // 51 | // is treated as an optional command arg and stripped out when processing the args 52 | // in types.go in the config package 53 | type CommandBasedAuthorizer struct { 54 | loggerProvider 55 | ctx context.Context 56 | body tq.AuthorRequest 57 | user config.User 58 | } 59 | 60 | // Handle will respond with failures or accepts as needed 61 | func (a CommandBasedAuthorizer) Handle(response tq.Response, request tq.Request) { 62 | if a.evaluate() { 63 | a.Debugf(request.Context, "authorized user [%v] as command based", a.user.Name) 64 | stringyHandleAuthorizeAcceptPassAdd.Inc() 65 | response.Reply( 66 | tq.NewAuthorReply( 67 | tq.SetAuthorReplyStatus(tq.AuthorStatusPassAdd), 68 | ), 69 | ) 70 | return 71 | } 72 | a.Debugf(request.Context, "user [%v] failed command based authorization", a.user.Name) 73 | stringyHandleAuthorizeFail.Inc() 74 | response.Reply( 75 | tq.NewAuthorReply( 76 | tq.SetAuthorReplyStatus(tq.AuthorStatusFail), 77 | tq.SetAuthorReplyServerMsg("not authorized"), 78 | ), 79 | ) 80 | } 81 | 82 | func (a CommandBasedAuthorizer) evaluate() bool { 83 | cmd := a.body.Args.Command() 84 | returnBool := func(c config.Action) bool { 85 | switch c { 86 | case config.PERMIT: 87 | return true 88 | default: 89 | return false 90 | } 91 | } 92 | for _, c := range a.user.Commands { 93 | c.TrimSpace() 94 | if c.Name == "*" { 95 | // special condition of allow anything 96 | return returnBool(c.Action) 97 | } 98 | if c.Name != cmd { 99 | continue 100 | } 101 | if len(c.Match) == 0 { 102 | // cmd matches, but we have no conditions, so match it 103 | return returnBool(c.Action) 104 | } 105 | 106 | for _, regexish := range c.Match { 107 | if len(regexish) == 0 { 108 | continue 109 | } 110 | // guard against regexes that are not anchored to the start and end of the string 111 | if regexish[0] != regexStartByte { 112 | regexish = regexStartStr + regexish 113 | } 114 | if regexish[len(regexish)-1] != regexEndByte { 115 | regexish = regexish + regexEndStr 116 | } 117 | if matched, err := regexp.MatchString(regexish, a.body.Args.CommandArgsNoLE()); err != nil { 118 | a.Errorf(a.ctx, "bad regex detected; %v", err) 119 | return false 120 | } else if matched { 121 | return returnBool(c.Action) 122 | } 123 | } 124 | } 125 | return false 126 | } 127 | -------------------------------------------------------------------------------- /cmds/server/config/authorizers/stringy/command_v2.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package stringy implements the only authorizer package available in tacquito. 9 | package stringy 10 | 11 | import ( 12 | "context" 13 | "regexp" 14 | 15 | tq "github.com/facebookincubator/tacquito" 16 | "github.com/facebookincubator/tacquito/cmds/server/config" 17 | ) 18 | 19 | // NewCommandBasedAuthorizerV2 will return a new authorizer that splits req args based on delimiter such as "|" and treats the delimited 20 | // args as separate inputs. If initial request params are not suitable for command based, it returns nil 21 | func NewCommandBasedAuthorizerV2(ctx context.Context, l loggerProvider, b tq.AuthorRequest, u config.User) *CommandBasedAuthorizerV2 { 22 | // commands are also only evaluated if service == shell 23 | if b.Args.Service() != "shell" { 24 | return nil 25 | } 26 | // cmd= and cmd* are not allowed. the command cmd=show etc must be specified 27 | // cmd*show is also not allowed. Only the mandatory separator is considered valid 28 | a, s, v := b.Args.CommandSplit() 29 | if a != "cmd" || s != "=" || v == "" { 30 | return nil 31 | } 32 | return &CommandBasedAuthorizerV2{ctx: ctx, loggerProvider: l, body: b, user: u} 33 | } 34 | 35 | // CommandBasedAuthorizerV2 provides a command based authorizer which works similar to the 36 | // original CommandBasedAuthorizer, but with a difference that it treats req args as a list of commands 37 | // split on a delimiter such as "|" 38 | type CommandBasedAuthorizerV2 struct { 39 | loggerProvider 40 | ctx context.Context 41 | body tq.AuthorRequest 42 | user config.User 43 | } 44 | 45 | // Handle will respond with failures or accepts as needed 46 | func (a CommandBasedAuthorizerV2) Handle(response tq.Response, request tq.Request) { 47 | if splits := a.body.Args.Split("|"); splits != nil { 48 | for _, args := range splits { 49 | if !a.evaluate(args) { 50 | a.Debugf(request.Context, "user [%v] failed command based authorization. Args=%v", a.user.Name, args) 51 | stringyHandleAuthorizeFailv2.Inc() 52 | response.Reply( 53 | tq.NewAuthorReply( 54 | tq.SetAuthorReplyStatus(tq.AuthorStatusFail), 55 | tq.SetAuthorReplyServerMsg("not authorized"), 56 | ), 57 | ) 58 | return 59 | } 60 | } 61 | } else { 62 | a.Debugf(request.Context, "user [%v] failed command based authorization; command had more than %d delimiters", a.user.Name, tq.MaxSplitCount) 63 | response.Reply( 64 | tq.NewAuthorReply( 65 | tq.SetAuthorReplyStatus(tq.AuthorStatusFail), 66 | tq.SetAuthorReplyServerMsg("not authorized"), 67 | ), 68 | ) 69 | return 70 | } 71 | 72 | stringyHandleAuthorizeAcceptPassAddv2.Inc() 73 | response.Reply( 74 | tq.NewAuthorReply( 75 | tq.SetAuthorReplyStatus(tq.AuthorStatusPassAdd), 76 | ), 77 | ) 78 | } 79 | 80 | func (a CommandBasedAuthorizerV2) evaluate(args tq.Args) bool { 81 | cmd := args.Command() 82 | cmdArgs := args.CommandArgsNoLE() 83 | 84 | returnBool := func(c config.Action) bool { 85 | switch c { 86 | case config.PERMIT: 87 | return true 88 | default: 89 | return false 90 | } 91 | } 92 | for _, c := range a.user.Commands { 93 | c.TrimSpace() 94 | if c.Name == "*" { 95 | // special condition of allow anything 96 | return returnBool(c.Action) 97 | } 98 | if c.Name != cmd { 99 | continue 100 | } 101 | if len(c.Match) == 0 { 102 | // cmd matches, but we have no conditions, so match it 103 | return returnBool(c.Action) 104 | } 105 | 106 | for _, regexish := range c.Match { 107 | if len(regexish) == 0 { 108 | continue 109 | } 110 | // guard against regexes that are not anchored to the start and end of the string 111 | if regexish[0] != regexStartByte { 112 | regexish = regexStartStr + regexish 113 | } 114 | if regexish[len(regexish)-1] != regexEndByte { 115 | regexish = regexish + regexEndStr 116 | } 117 | if matched, err := regexp.MatchString(regexish, cmdArgs); err != nil { 118 | a.Errorf(a.ctx, "bad regex detected; %v", err) 119 | return false 120 | } else if matched { 121 | return returnBool(c.Action) 122 | } 123 | } 124 | } 125 | return false 126 | } 127 | -------------------------------------------------------------------------------- /cmds/server/config/authorizers/stringy/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package stringy 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "log" 14 | "os" 15 | ) 16 | 17 | // NewDefaultLogger provides a basic logger for tests 18 | func NewDefaultLogger() *DefaultLogger { 19 | return &DefaultLogger{ 20 | ErrorLogger: log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Llongfile), 21 | InfoLogger: log.New(os.Stderr, "INFO: ", log.Ldate|log.Ltime|log.Llongfile), 22 | DebugLogger: log.New(os.Stderr, "DEBUG: ", log.Ldate|log.Ltime|log.Llongfile), 23 | } 24 | } 25 | 26 | // DefaultLogger ... 27 | type DefaultLogger struct { 28 | // ErrorLogger is Level Error Logger 29 | ErrorLogger *log.Logger 30 | // InfoLogger is Level Info Logger 31 | InfoLogger *log.Logger 32 | // DebugLogger is a Level Debug Logger 33 | DebugLogger *log.Logger 34 | } 35 | 36 | // Errorf ... 37 | func (d DefaultLogger) Errorf(ctx context.Context, format string, args ...interface{}) { 38 | d.ErrorLogger.Output(2, fmt.Sprintf(format, args...)) 39 | } 40 | 41 | // Infof ... 42 | func (d DefaultLogger) Infof(ctx context.Context, format string, args ...interface{}) { 43 | d.InfoLogger.Output(2, fmt.Sprintf(format, args...)) 44 | } 45 | 46 | // Debugf ... 47 | func (d DefaultLogger) Debugf(ctx context.Context, format string, args ...interface{}) { 48 | d.DebugLogger.Output(2, fmt.Sprintf(format, args...)) 49 | } 50 | -------------------------------------------------------------------------------- /cmds/server/config/authorizers/stringy/stats.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package stringy 9 | 10 | import ( 11 | "github.com/prometheus/client_golang/prometheus" 12 | ) 13 | 14 | var ( 15 | // see https://datatracker.ietf.org/doc/html/rfc8907#section-6.2 for pass add/replace 16 | stringyHandleAuthorizeAcceptPassReplace = prometheus.NewCounter(prometheus.CounterOpts{ 17 | Namespace: "tacquito", 18 | Name: "stringy_handle_authorize_accept_pass_replace", 19 | Help: "number of stringy authorize accept pass replace packets", 20 | }) 21 | stringyHandleAuthorizeAcceptPassAdd = prometheus.NewCounter(prometheus.CounterOpts{ 22 | Namespace: "tacquito", 23 | Name: "stringy_handle_authorize_accept_pass_add", 24 | Help: "number of stringy authorize accept pass add packets", 25 | }) 26 | stringyHandleAuthorizeFail = prometheus.NewCounter(prometheus.CounterOpts{ 27 | Namespace: "tacquito", 28 | Name: "stringy_handle_authorize_fail", 29 | Help: "number of stringy authorize fail packets", 30 | }) 31 | stringyHandleAuthorizeError = prometheus.NewCounter(prometheus.CounterOpts{ 32 | Namespace: "tacquito", 33 | Name: "stringy_handle_authorize_error", 34 | Help: "number of stringy authorize error packets", 35 | }) 36 | stringyHandleUnexpectedPacket = prometheus.NewCounter(prometheus.CounterOpts{ 37 | Namespace: "tacquito", 38 | Name: "stringy_handle_unexpected_packet", 39 | Help: "number of stringy handle unexpected packets", 40 | }) 41 | 42 | // v2 43 | stringyHandleAuthorizeAcceptPassAddv2 = prometheus.NewCounter(prometheus.CounterOpts{ 44 | Namespace: "tacquito", 45 | Name: "stringy_handle_authorize_accept_pass_add_v2", 46 | Help: "number of stringy authorize accept pass add packets", 47 | }) 48 | stringyHandleAuthorizeFailv2 = prometheus.NewCounter(prometheus.CounterOpts{ 49 | Namespace: "tacquito", 50 | Name: "stringy_handle_authorize_fail_v2", 51 | Help: "number of stringy authorize fail packets", 52 | }) 53 | ) 54 | 55 | func init() { 56 | prometheus.MustRegister(stringyHandleAuthorizeAcceptPassReplace) 57 | prometheus.MustRegister(stringyHandleAuthorizeAcceptPassAdd) 58 | prometheus.MustRegister(stringyHandleAuthorizeAcceptPassAddv2) 59 | prometheus.MustRegister(stringyHandleAuthorizeFail) 60 | prometheus.MustRegister(stringyHandleAuthorizeFailv2) 61 | prometheus.MustRegister(stringyHandleAuthorizeError) 62 | prometheus.MustRegister(stringyHandleUnexpectedPacket) 63 | } 64 | -------------------------------------------------------------------------------- /cmds/server/config/authorizers/stringy/stringy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package stringy behaves in a similar way to the tacplus cisco/shurbbery implementation 9 | // it's just string matching + regex and hope 10 | package stringy 11 | 12 | import ( 13 | "context" 14 | 15 | tq "github.com/facebookincubator/tacquito" 16 | "github.com/facebookincubator/tacquito/cmds/server/config" 17 | ) 18 | 19 | // loggerProvider provides the logging implementation 20 | type loggerProvider interface { 21 | Infof(ctx context.Context, format string, args ...interface{}) 22 | Errorf(ctx context.Context, format string, args ...interface{}) 23 | Debugf(ctx context.Context, format string, args ...interface{}) 24 | } 25 | 26 | // Option is a functional option for the authorizer 27 | type Option func(*Authorizer) 28 | 29 | // EnableCmdV2 is a functional option to enable the new CommandBasedAuthorizerV2 instance 30 | func EnableCmdV2(enableV2 bool) Option { 31 | return func(a *Authorizer) { 32 | a.enableCmdV2 = enableV2 33 | } 34 | } 35 | 36 | // New stringy Authorizer 37 | func New(l loggerProvider, opts ...Option) *Authorizer { 38 | a := &Authorizer{loggerProvider: l} 39 | for _, opt := range opts { 40 | opt(a) 41 | } 42 | return a 43 | } 44 | 45 | // Authorizer is for authorization of commands and such 46 | type Authorizer struct { 47 | loggerProvider 48 | user config.User 49 | 50 | // enableCmdV2 is a flag to enable the new CommandBasedAuthorizerV2 instance 51 | enableCmdV2 bool 52 | } 53 | 54 | // New creates a new stringy authorizer which implements tq.Handler 55 | func (a Authorizer) New(user config.User) (tq.Handler, error) { 56 | // ReduceAll appends all group level services and commands to the user level 57 | // user level overrides for services and commands are processed first, then the groups. 58 | a.ReduceAll(&user) 59 | return &Authorizer{ 60 | loggerProvider: a.loggerProvider, 61 | user: user, 62 | enableCmdV2: a.enableCmdV2, 63 | }, nil 64 | } 65 | 66 | // ReduceAll will collapse all services and commands down to the user level 67 | func (a Authorizer) ReduceAll(u *config.User) { 68 | for _, g := range u.Groups { 69 | u.Services = append(u.Services, g.Services...) 70 | u.Commands = append(u.Commands, g.Commands...) 71 | } 72 | } 73 | 74 | // Handle handles all authenticate message types, scoped to the uid 75 | func (a Authorizer) Handle(response tq.Response, request tq.Request) { 76 | var body tq.AuthorRequest 77 | if err := tq.Unmarshal(request.Body, &body); err != nil { 78 | stringyHandleUnexpectedPacket.Inc() 79 | stringyHandleAuthorizeError.Inc() 80 | response.Reply( 81 | tq.NewAuthorReply( 82 | tq.SetAuthorReplyStatus(tq.AuthorStatusError), 83 | tq.SetAuthorReplyServerMsg("unable to decode AuthorRequest packet"), 84 | ), 85 | ) 86 | return 87 | } 88 | 89 | if a.enableCmdV2 { 90 | if authorizer := NewCommandBasedAuthorizerV2(request.Context, a.loggerProvider, body, a.user); authorizer != nil { 91 | a.Debugf(request.Context, "detected user [%v] using command based authorization", a.user.Name) 92 | authorizer.Handle(response, request) 93 | return 94 | } 95 | } else { 96 | if authorizer := NewCommandBasedAuthorizer(request.Context, a.loggerProvider, body, a.user); authorizer != nil { 97 | a.Debugf(request.Context, "detected user [%v] using command based authorization", a.user.Name) 98 | authorizer.Handle(response, request) 99 | return 100 | } 101 | } 102 | 103 | if authorizer := NewSessionBasedAuthorizer(request.Context, a.loggerProvider, body, a.user); authorizer != nil { 104 | a.Debugf(request.Context, "detected user [%v] using session based authorization", a.user.Name) 105 | authorizer.Handle(response, request) 106 | return 107 | } 108 | 109 | a.Debugf(request.Context, "failed to authorize the user: [%v]", a.user.Name) 110 | stringyHandleAuthorizeFail.Inc() 111 | response.Reply( 112 | tq.NewAuthorReply( 113 | tq.SetAuthorReplyStatus(tq.AuthorStatusFail), 114 | tq.SetAuthorReplyServerMsg("not authorized"), 115 | ), 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /cmds/server/config/authorizers/stringy/test/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package test 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "log" 14 | "os" 15 | ) 16 | 17 | // loggerProvider provides the logging implementation 18 | type loggerProvider interface { 19 | Infof(ctx context.Context, format string, args ...interface{}) 20 | Errorf(ctx context.Context, format string, args ...interface{}) 21 | Debugf(ctx context.Context, format string, args ...interface{}) 22 | } 23 | 24 | // newDefaultLogger provides a basic logger if one is not provided 25 | // levels: 10 error, 20 info, 30 debug. fatal has no level 26 | func newDefaultLogger(level int) *defaultLogger { 27 | return &defaultLogger{ 28 | level: level, 29 | ErrorLogger: log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Llongfile), 30 | InfoLogger: log.New(os.Stderr, "INFO: ", log.Ldate|log.Ltime|log.Llongfile), 31 | DebugLogger: log.New(os.Stderr, "DEBUG: ", log.Ldate|log.Ltime|log.Llongfile), 32 | } 33 | } 34 | 35 | // defaultLogger ... 36 | type defaultLogger struct { 37 | // log level to use 38 | level int 39 | // ErrorLogger is Level Error Logger 40 | ErrorLogger *log.Logger 41 | // InfoLogger is Level Info Logger 42 | InfoLogger *log.Logger 43 | // DebugLogger is a Level Debug Logger 44 | DebugLogger *log.Logger 45 | } 46 | 47 | // Errorf ... 48 | func (d defaultLogger) Errorf(ctx context.Context, format string, args ...interface{}) { 49 | if d.level >= 10 { 50 | d.ErrorLogger.Output(2, fmt.Sprintf(format, args...)) 51 | } 52 | } 53 | 54 | // Infof ... 55 | func (d defaultLogger) Infof(ctx context.Context, format string, args ...interface{}) { 56 | if d.level >= 20 { 57 | d.InfoLogger.Output(2, fmt.Sprintf(format, args...)) 58 | } 59 | } 60 | 61 | // Debugf ... 62 | func (d defaultLogger) Debugf(ctx context.Context, format string, args ...interface{}) { 63 | if d.level >= 30 { 64 | d.DebugLogger.Output(2, fmt.Sprintf(format, args...)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmds/server/config/provider.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package config provides an example implementation of the tacquito.ConfigProvider interface. 9 | package config 10 | 11 | // Provider ... 12 | type Provider interface { 13 | GetUser(user string) *AAA 14 | } 15 | 16 | // New returns a tacquito.ConfigProvider that maps a scoped username to a given 17 | // SecretConfig. 18 | func New() AAAProvider { 19 | return make(map[string]*AAA) 20 | } 21 | 22 | // AAAProvider gives us scoped AAA types, which are a wrapped User type 23 | type AAAProvider map[string]*AAA 24 | 25 | // New returns a scoped provider for users 26 | func (s AAAProvider) New(users map[string]*AAA) Provider { 27 | return AAAProvider(users) 28 | } 29 | 30 | // GetUser gets the handlers.Config that is associated to a username 31 | func (s AAAProvider) GetUser(username string) *AAA { 32 | return s[username] 33 | } 34 | -------------------------------------------------------------------------------- /cmds/server/config/secret/dns/provider.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dns 9 | 10 | import ( 11 | "context" 12 | "encoding/json" 13 | "fmt" 14 | "net" 15 | 16 | tq "github.com/facebookincubator/tacquito" 17 | "github.com/facebookincubator/tacquito/cmds/server/config" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | ) 21 | 22 | // loggerProvider provides the logging implementation 23 | type loggerProvider interface { 24 | Infof(ctx context.Context, format string, args ...interface{}) 25 | Errorf(ctx context.Context, format string, args ...interface{}) 26 | Debugf(ctx context.Context, format string, args ...interface{}) 27 | } 28 | 29 | // ProviderOption is the setter type for Provider 30 | type ProviderOption func(p *Provider) 31 | 32 | // SetDNSSecret will set a secret config for a given hostname 33 | func SetDNSSecret(config secretConfig, hosts ...string) ProviderOption { 34 | return func(p *Provider) { 35 | for _, h := range hosts { 36 | p.secrets[h] = config 37 | } 38 | } 39 | } 40 | 41 | // SetLoggerProvider will set a logger to use 42 | func SetLoggerProvider(l loggerProvider) ProviderOption { 43 | return func(p *Provider) { 44 | p.loggerProvider = l 45 | } 46 | } 47 | 48 | // New creates new config sources based on users, groups and services 49 | func New(l loggerProvider, opts ...ProviderOption) *Provider { 50 | s := &Provider{loggerProvider: l, secrets: make(map[string]secretConfig)} 51 | for _, opt := range opts { 52 | opt(s) 53 | } 54 | return s 55 | } 56 | 57 | // Provider ... 58 | type Provider struct { 59 | loggerProvider 60 | secrets map[string]secretConfig 61 | } 62 | 63 | // New returns a scoped Provider for a given set of users. 64 | func (p *Provider) New(ctx context.Context, provider config.SecretConfig, handler tq.Handler, secret func(context.Context, string) ([]byte, error)) tq.SecretProvider { 65 | var hosts []string 66 | err := json.Unmarshal([]byte(provider.Options["hosts"]), &hosts) 67 | if err != nil { 68 | p.Errorf(ctx, "unable to unmarshal key [hosts] on dns based secret provider [%v]; %v", provider.Name, err) 69 | return nil 70 | } 71 | if len(hosts) == 0 { 72 | p.Errorf(ctx, "no host provided for dns based secret provider [%v]", provider.Name) 73 | return nil 74 | } 75 | 76 | scopedConfig := secretConfig{ 77 | secret: secret, 78 | Handler: handler, 79 | } 80 | 81 | return New( 82 | p.loggerProvider, 83 | SetDNSSecret(scopedConfig, hosts...), 84 | ) 85 | } 86 | 87 | // Get returns a tq SecretProvider interface and or error 88 | func (p *Provider) Get(ctx context.Context, remote net.Addr) ([]byte, tq.Handler, error) { 89 | addr, ok := remote.(*net.TCPAddr) 90 | if !ok { 91 | return nil, nil, fmt.Errorf("unable to assert [%v] is net.TCPAddr", remote) 92 | } 93 | timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { 94 | ms := v * 1000 // make milliseconds 95 | dnsDurations.Observe(ms) 96 | })) 97 | names, err := net.LookupAddr(addr.IP.String()) 98 | if err != nil { 99 | timer.ObserveDuration() 100 | dnsError.Inc() 101 | return nil, nil, err 102 | } 103 | timer.ObserveDuration() 104 | for _, name := range names { 105 | if c, ok := p.secrets[name]; ok { 106 | dnsGetMatch.Inc() 107 | p.Debugf(ctx, "dns secret provider matches remote [%v] against fqdn [%v]", addr.IP.String(), name) 108 | secret, err := c.secret(ctx, name) 109 | return secret, c, err 110 | } 111 | } 112 | return nil, nil, fmt.Errorf("no matching dns secret provider found for names %v, for remote [%v]", names, addr.IP.String()) 113 | } 114 | 115 | // secretConfig holds the secret config needed for the SecretProvider 116 | type secretConfig struct { 117 | // Secret is applied when performing crypt/obfuscation ops 118 | secret func(context.Context, string) ([]byte, error) 119 | // Handler embeds our Handler interface scoped to this SecretConfig 120 | tq.Handler 121 | } 122 | -------------------------------------------------------------------------------- /cmds/server/config/secret/dns/stats.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dns 9 | 10 | import ( 11 | "github.com/prometheus/client_golang/prometheus" 12 | ) 13 | 14 | var ( 15 | // gauges and counters 16 | dnsGetMatch = prometheus.NewCounter(prometheus.CounterOpts{ 17 | Namespace: "tacquito", 18 | Name: "secret_provider_dns_get_match", 19 | Help: "number of dns secret provider matches", 20 | }) 21 | dnsError = prometheus.NewCounter(prometheus.CounterOpts{ 22 | Namespace: "tacquito", 23 | Name: "secret_provider_dns_get_error", 24 | Help: "the number of errors encountered when resolving dns", 25 | }) 26 | // durations 27 | dnsDurations = prometheus.NewSummary( 28 | prometheus.SummaryOpts{ 29 | Namespace: "tacquito", 30 | Name: "secret_provider_dns_query_duration_milliseconds", 31 | Help: "the time it takes for dns queries to respond, in milliseconds", 32 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, 33 | }, 34 | ) 35 | ) 36 | 37 | func init() { 38 | // gauges and counters 39 | prometheus.MustRegister(dnsGetMatch) 40 | prometheus.MustRegister(dnsError) 41 | // durations 42 | prometheus.MustRegister(dnsDurations) 43 | } 44 | -------------------------------------------------------------------------------- /cmds/server/config/secret/keychain.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package secret 9 | 10 | import ( 11 | "context" 12 | "github.com/facebookincubator/tacquito/cmds/server/config" 13 | ) 14 | 15 | // New keychain that provides the password via keychain.Key as pre-shared key to use in client calls for 16 | // tacacs obfuscation ops 17 | func New() *Keychain { 18 | return &Keychain{} 19 | } 20 | 21 | // Keychain is a default, unsafe pre-shared key provider 22 | type Keychain struct{} 23 | 24 | // Add returns the pre-shared tacacs key to be used with a connection 25 | func (k Keychain) Add(kc config.Keychain) func(context.Context, string) ([]byte, error) { 26 | // This is an example implementation only. 27 | // You should provide your own keychain implementation that takes the key and group from keychain 28 | // and stages this type to return a value from a trusted, secure store. We short circuit 29 | // to simply returning a static key as an example 30 | return func(ctx context.Context, username string) ([]byte, error) { 31 | return []byte(kc.Key), nil 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmds/server/config/secret/prefix/provider.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package prefix 9 | 10 | import ( 11 | "context" 12 | "encoding/json" 13 | "fmt" 14 | "net" 15 | 16 | tq "github.com/facebookincubator/tacquito" 17 | "github.com/facebookincubator/tacquito/cmds/server/config" 18 | ) 19 | 20 | // loggerProvider provides the logging implementation 21 | type loggerProvider interface { 22 | Infof(ctx context.Context, format string, args ...interface{}) 23 | Errorf(ctx context.Context, format string, args ...interface{}) 24 | Debugf(ctx context.Context, format string, args ...interface{}) 25 | Record(ctx context.Context, r map[string]string, obscure ...string) 26 | } 27 | 28 | // ProviderOption is the setter type for Provider 29 | type ProviderOption func(p *Provider) 30 | 31 | // SetPrefixSecret will set a secret config for a given prefix source 32 | // this could be a range that clients call in from or from specific hosts 33 | func SetPrefixSecret(config secretConfig, prefixes ...string) ProviderOption { 34 | return func(p *Provider) { 35 | for _, prefix := range prefixes { 36 | _, ipnet, err := net.ParseCIDR(prefix) 37 | if err != nil { 38 | continue 39 | } 40 | p.secrets[ipnet.String()] = config 41 | } 42 | } 43 | } 44 | 45 | // SetLoggerProvider will set a logger to use 46 | func SetLoggerProvider(l loggerProvider) ProviderOption { 47 | return func(p *Provider) { 48 | p.loggerProvider = l 49 | } 50 | } 51 | 52 | // New creates new config sources based on users, groups and services 53 | func New(l loggerProvider, opts ...ProviderOption) *Provider { 54 | s := &Provider{ 55 | loggerProvider: l, 56 | secrets: make(map[string]secretConfig), 57 | } 58 | for _, opt := range opts { 59 | opt(s) 60 | } 61 | return s 62 | } 63 | 64 | // Provider ... 65 | type Provider struct { 66 | loggerProvider 67 | secrets map[string]secretConfig 68 | } 69 | 70 | // New returns a scoped Provider for a given set of users. 71 | func (p *Provider) New(ctx context.Context, provider config.SecretConfig, handler tq.Handler, secret func(context.Context, string) ([]byte, error)) tq.SecretProvider { 72 | var prefixes []string 73 | raw := provider.Options["prefixes"] 74 | if err := json.Unmarshal([]byte(raw), &prefixes); err != nil { 75 | p.Errorf(ctx, "missing prefixes key in options for prefix based secret provider [%v]", provider.Name) 76 | return nil 77 | } 78 | if len(prefixes) == 0 { 79 | p.Errorf(ctx, "no prefixes provided for prefix based secret provider [%v]", provider.Name) 80 | return nil 81 | } 82 | scopedConfig := secretConfig{ 83 | secret: secret, 84 | Handler: handler, 85 | } 86 | return New( 87 | p.loggerProvider, 88 | SetPrefixSecret(scopedConfig, prefixes...), 89 | ) 90 | } 91 | 92 | // Get returns a tq SecretProvider interface and or error 93 | func (p *Provider) Get(ctx context.Context, remote net.Addr) ([]byte, tq.Handler, error) { 94 | addr, ok := remote.(*net.TCPAddr) 95 | if !ok { 96 | return nil, nil, fmt.Errorf("unable to assert [%v] is net.TCPAddr", remote) 97 | } 98 | for cidr, c := range p.secrets { 99 | _, ipNet, err := net.ParseCIDR(cidr) 100 | if err != nil { 101 | p.Errorf(ctx, "error parsing ip from SecretProvider: %v", err) 102 | continue 103 | } 104 | if ipNet.Contains(addr.IP) { 105 | p.Debugf(ctx, "prefix secret provider matches remote [%v] against prefix [%v]", addr.IP.String(), cidr) 106 | secret, err := c.secret(ctx, addr.IP.String()) 107 | return secret, c, err 108 | } 109 | } 110 | return nil, nil, fmt.Errorf("no matching prefix secret provider found") 111 | } 112 | 113 | // secretConfig holds the secret config needed for the SecretProvider 114 | type secretConfig struct { 115 | // Secret is applied when performing crypt/obfuscation ops 116 | secret func(context.Context, string) ([]byte, error) 117 | // Handler embeds our Handler interface scoped to this SecretConfig 118 | tq.Handler 119 | } 120 | -------------------------------------------------------------------------------- /cmds/server/exporter/exporter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package exporter 9 | 10 | import ( 11 | "flag" 12 | "log" 13 | "net/http" 14 | _ "net/http/pprof" 15 | 16 | "github.com/prometheus/client_golang/prometheus/promhttp" 17 | ) 18 | 19 | var ( 20 | promExportAddress = flag.String("metrics-address", ":8080", "port for promhttp exporter to listen on") 21 | exportPromHTTP = flag.Bool("export-promhttp", true, "execute promHttp handler") 22 | ) 23 | 24 | // StartPromHTTP will start the prometheus http service that reports our metrics 25 | func StartPromHTTP() error { 26 | if *exportPromHTTP { 27 | http.Handle("/metrics", promhttp.Handler()) 28 | log.Printf("starting prometheus http exporter, listening [%v]/metrics", *promExportAddress) 29 | return http.ListenAndServe(*promExportAddress, nil) 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /cmds/server/handlers/acct.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "fmt" 12 | 13 | tq "github.com/facebookincubator/tacquito" 14 | ) 15 | 16 | // NewAccountingRequest ... 17 | func NewAccountingRequest(l loggerProvider, c configProvider) *AccountingRequest { 18 | return &AccountingRequest{loggerProvider: l, configProvider: c, recorderWriter: newPacketLogger(l)} 19 | } 20 | 21 | // AccountingRequest is the main entry point for incoming AcctRequest packets 22 | type AccountingRequest struct { 23 | loggerProvider 24 | configProvider 25 | recorderWriter 26 | } 27 | 28 | // Handle ... 29 | func (a *AccountingRequest) Handle(response tq.Response, request tq.Request) { 30 | var body tq.AcctRequest 31 | if err := tq.Unmarshal(request.Body, &body); err != nil { 32 | a.Errorf(request.Context, "unable to unmarshal accounting packet : %v", err) 33 | accountingHandleUnexpectedPacket.Inc() 34 | accountingHandleError.Inc() 35 | response.ReplyWithContext( 36 | request.Context, 37 | tq.NewAcctReply( 38 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 39 | tq.SetAcctReplyServerMsg("expected accounting request packet"), 40 | ), 41 | a.recorderWriter, 42 | ) 43 | return 44 | } 45 | 46 | a.RecordCtx(&request, tq.ContextUser, tq.ContextRemoteAddr, tq.ContextReqArgs, tq.ContextAcctType, tq.ContextPort, tq.ContextPrivLvl, tq.ContextFlags) 47 | // TODO implement a fallback for cases where a username may not be present. 48 | c := a.GetUser(string(body.User)) 49 | if c == nil { 50 | a.Errorf(request.Context, "[%v] user [%v] does not have an accounter associated", request.Header.SessionID, body.User) 51 | accountingHandleAccounterNil.Inc() 52 | response.ReplyWithContext( 53 | a.Context(), 54 | tq.NewAcctReply( 55 | tq.SetAcctReplyStatus(tq.AcctReplyStatusError), 56 | // ensure user field is present in accounting packet, it could cause this. 57 | tq.SetAcctReplyServerMsg(fmt.Sprintf("failed to lookup user [%s] for accounting login", string(body.User))), 58 | ), 59 | a.recorderWriter, 60 | ) 61 | return 62 | } 63 | 64 | NewResponseLogger(a.Context(), a.loggerProvider, c.Accounting).Handle(response, request) 65 | } 66 | -------------------------------------------------------------------------------- /cmds/server/handlers/authen.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "fmt" 12 | 13 | tq "github.com/facebookincubator/tacquito" 14 | ) 15 | 16 | // NewAuthenticateStart ... 17 | func NewAuthenticateStart(l loggerProvider, c configProvider) *AuthenticateStart { 18 | return &AuthenticateStart{loggerProvider: l, configProvider: c, recorderWriter: newPacketLogger(l)} 19 | } 20 | 21 | // AuthenticateStart is the main entry point for incoming authenstart packets 22 | type AuthenticateStart struct { 23 | loggerProvider 24 | configProvider 25 | recorderWriter 26 | } 27 | 28 | // authenActionStart is a function map that determines which authenticate handler to call given 29 | // the constraints per the rfc when examining action, type and minor version. 30 | type authenActionStart struct { 31 | action tq.AuthenAction 32 | atype tq.AuthenType 33 | service tq.AuthenService 34 | minorVersion uint8 35 | } 36 | 37 | // Handle ... 38 | func (a *AuthenticateStart) Handle(response tq.Response, request tq.Request) { 39 | var body tq.AuthenStart 40 | if err := tq.Unmarshal(request.Body, &body); err != nil { 41 | authenStartHandleUnexpectedPacket.Inc() 42 | authenStartHandleError.Inc() 43 | response.ReplyWithContext( 44 | request.Context, 45 | tq.NewAuthenReply( 46 | tq.SetAuthenReplyStatus(tq.AuthenStatusError), 47 | tq.SetAuthenReplyServerMsg(fmt.Sprintf("expected authenticate start packet for sessionID [%v]", request.Header.SessionID)), 48 | ), 49 | a.recorderWriter, 50 | ) 51 | return 52 | } 53 | 54 | authenRouter := map[authenActionStart]tq.Handler{ 55 | // 5.4.2.6. Enable Requests 56 | {action: tq.AuthenActionLogin, service: tq.AuthenServiceEnable, minorVersion: tq.MinorVersionOne}: NewAuthenticateASCII(a.loggerProvider, a.configProvider, string(body.User)), 57 | // 5.4.2.1. ASCII Login Requests 58 | {action: tq.AuthenActionLogin, atype: tq.AuthenTypeASCII, minorVersion: tq.MinorVersionDefault}: NewAuthenticateASCII(a.loggerProvider, a.configProvider, string(body.User)), 59 | // 5.4.2.2. PAP Login Requests 60 | {action: tq.AuthenActionLogin, atype: tq.AuthenTypePAP, minorVersion: tq.MinorVersionOne}: NewAuthenticatePAP(a.loggerProvider, a.configProvider), 61 | {action: tq.AuthenActionLogin, atype: tq.AuthenTypeCHAP, minorVersion: tq.MinorVersionOne}: nil, //AuthenCHAPStart not implemented 62 | {action: tq.AuthenActionLogin, atype: tq.AuthenTypeMSCHAP, minorVersion: tq.MinorVersionOne}: nil, //AuthenMSCHAPStart not implemented 63 | {action: tq.AuthenActionLogin, atype: tq.AuthenTypeMSCHAPV2, minorVersion: tq.MinorVersionOne}: nil, //AuthenMSCHAPV2Start not implemented 64 | } 65 | key := authenActionStart{action: body.Action, atype: body.Type, minorVersion: request.Header.Version.MinorVersion} 66 | if h := authenRouter[key]; h != nil { 67 | h.Handle(response, request) 68 | return 69 | } 70 | // we don't know what this packet is, so we log everything in it. this could log passwords but w/o knowing what this 71 | // packet was, we can't effectively omit fields, so we guess. user-msg may contain a password. 72 | a.Record(request.Context, request.Fields(tq.ContextConnRemoteAddr, tq.ContextConnLocalAddr), "user-msg") 73 | authenStartHandleUnexpectedPacket.Inc() 74 | authenStartHandleError.Inc() 75 | response.ReplyWithContext( 76 | request.Context, 77 | tq.NewAuthenReply( 78 | tq.SetAuthenReplyStatus(tq.AuthenStatusError), 79 | tq.SetAuthenReplyServerMsg("unknown authenticate start packet type"), 80 | ), 81 | a.recorderWriter, 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /cmds/server/handlers/authen_pap.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "fmt" 12 | 13 | tq "github.com/facebookincubator/tacquito" 14 | ) 15 | 16 | // NewAuthenticatePAP creates a scoped handler for PAP authentication exchanges 17 | func NewAuthenticatePAP(l loggerProvider, c configProvider) *AuthenticatePAP { 18 | return &AuthenticatePAP{loggerProvider: l, configProvider: c, recorderWriter: newPacketLogger(l)} 19 | } 20 | 21 | // AuthenticatePAP is the main entry for pap authenticate exchanges 22 | type AuthenticatePAP struct { 23 | loggerProvider 24 | configProvider 25 | recorderWriter 26 | username string 27 | } 28 | 29 | // Handle requires that the username and password be present in a AuthenStart packet. 30 | func (a *AuthenticatePAP) Handle(response tq.Response, request tq.Request) { 31 | authenStartHandlePAP.Inc() 32 | var body tq.AuthenStart 33 | if err := tq.Unmarshal(request.Body, &body); err != nil { 34 | authenPAPHandleUnexpectedPacket.Inc() 35 | authenASCIIHandleAuthenError.Inc() 36 | response.ReplyWithContext( 37 | request.Context, 38 | tq.NewAuthenReply( 39 | tq.SetAuthenReplyStatus(tq.AuthenStatusError), 40 | tq.SetAuthenReplyServerMsg("unable to decode authenticate start packet"), 41 | ), 42 | a.recorderWriter, 43 | ) 44 | return 45 | } 46 | // missing username 47 | if len(body.User) == 0 { 48 | a.Debugf(request.Context, "[%v] [%v] username is missing for rem-addr: [%v]", request.Header.SessionID, body.RemAddr) 49 | authenPAPHandleAuthenError.Inc() 50 | authenPAPHandleMissingUsername.Inc() 51 | response.ReplyWithContext( 52 | request.Context, 53 | tq.NewAuthenReply( 54 | tq.SetAuthenReplyStatus(tq.AuthenStatusError), 55 | tq.SetAuthenReplyServerMsg("missing username"), 56 | ), 57 | a.recorderWriter, 58 | ) 59 | return 60 | } 61 | a.RecordCtx(&request, tq.ContextUser, tq.ContextRemoteAddr, tq.ContextPort, tq.ContextPrivLvl) 62 | // missing password 63 | if len(body.Data) == 0 { 64 | a.Debugf(request.Context, "[%v] [%v] username [%v] is missing a password for rem-addr: [%v]", request.Header.SessionID, body.User, body.RemAddr) 65 | authenPAPHandleMissingPassword.Inc() 66 | authenPAPHandleAuthenError.Inc() 67 | response.ReplyWithContext( 68 | a.Context(), 69 | tq.NewAuthenReply( 70 | tq.SetAuthenReplyStatus(tq.AuthenStatusFail), 71 | tq.SetAuthenReplyServerMsg("missing password"), 72 | ), 73 | a.recorderWriter, 74 | ) 75 | return 76 | } 77 | c := a.GetUser(string(body.User)) 78 | if c == nil { 79 | a.Debugf(request.Context, "[%v] user [%v] does not have an authenticator associated", request.Header.SessionID, body.User) 80 | authenPAPHandleAuthenFail.Inc() 81 | authenPAPHandleAuthenticatorNil.Inc() 82 | response.ReplyWithContext( 83 | a.Context(), 84 | tq.NewAuthenReply( 85 | tq.SetAuthenReplyStatus(tq.AuthenStatusFail), 86 | tq.SetAuthenReplyServerMsg(fmt.Sprintf("authentication denied [%s]", string(body.User))), 87 | ), 88 | a.recorderWriter, 89 | ) 90 | return 91 | } 92 | NewResponseLogger(a.Context(), a.loggerProvider, c.Authenticate).Handle(response, request) 93 | } 94 | -------------------------------------------------------------------------------- /cmds/server/handlers/author.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "fmt" 12 | 13 | tq "github.com/facebookincubator/tacquito" 14 | ) 15 | 16 | // NewAuthorizeRequest ... 17 | func NewAuthorizeRequest(l loggerProvider, c configProvider) *AuthorizeRequest { 18 | return &AuthorizeRequest{loggerProvider: l, configProvider: c, recorderWriter: newPacketLogger(l)} 19 | } 20 | 21 | // AuthorizeRequest is the main entry point for incoming AuthorRequest packets 22 | type AuthorizeRequest struct { 23 | loggerProvider 24 | configProvider 25 | recorderWriter 26 | } 27 | 28 | // Handle ... 29 | func (a *AuthorizeRequest) Handle(response tq.Response, request tq.Request) { 30 | var body tq.AuthorRequest 31 | if err := tq.Unmarshal(request.Body, &body); err != nil { 32 | a.Errorf(request.Context, "failed to unmarshal AuthorRequest [%v]", err) 33 | authorizerHandleUnexpectedPacket.Inc() 34 | authorizerHandleError.Inc() 35 | response.ReplyWithContext( 36 | request.Context, 37 | tq.NewAuthorReply( 38 | tq.SetAuthorReplyStatus(tq.AuthorStatusError), 39 | tq.SetAuthorReplyServerMsg("invalid AuthorRequest packet"), 40 | ), 41 | a.recorderWriter, 42 | ) 43 | return 44 | } 45 | a.RecordCtx(&request, tq.ContextUser, tq.ContextRemoteAddr, tq.ContextReqArgs, tq.ContextPort, tq.ContextPrivLvl) 46 | c := a.GetUser(string(body.User)) 47 | if c == nil { 48 | a.Errorf(request.Context, "[%v] user [%v] does not have an authorizer associated", request.Header.SessionID, body.User) 49 | authorizerHandleAuthorizerNil.Inc() 50 | response.ReplyWithContext( 51 | a.Context(), 52 | tq.NewAuthorReply( 53 | tq.SetAuthorReplyStatus(tq.AuthorStatusFail), 54 | tq.SetAuthorReplyServerMsg( 55 | fmt.Sprintf("authorization denied for user [%s]", string(body.User)), 56 | ), 57 | ), 58 | a.recorderWriter, 59 | ) 60 | return 61 | } 62 | NewResponseLogger(a.Context(), a.loggerProvider, c.Authorizer).Handle(response, request) 63 | } 64 | -------------------------------------------------------------------------------- /cmds/server/handlers/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "github.com/facebookincubator/tacquito/cmds/server/config" 12 | ) 13 | 14 | // configProvider provides access to user level AAA operations with a fallback for global 15 | // All implementations must be compatible with a concurrent access model. Non-threadsafe 16 | // implementations are not recommended. 17 | 18 | type configProvider interface { 19 | GetUser(user string) *config.AAA 20 | } 21 | -------------------------------------------------------------------------------- /cmds/server/handlers/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "context" 12 | 13 | tq "github.com/facebookincubator/tacquito" 14 | ) 15 | 16 | // loggerProvider provides the logging implementation 17 | type loggerProvider interface { 18 | Infof(ctx context.Context, format string, args ...interface{}) 19 | Errorf(ctx context.Context, format string, args ...interface{}) 20 | Debugf(ctx context.Context, format string, args ...interface{}) 21 | Record(ctx context.Context, r map[string]string, obscure ...string) 22 | Set(ctx context.Context, fields map[string]string, keys ...tq.ContextKey) context.Context 23 | } 24 | -------------------------------------------------------------------------------- /cmds/server/handlers/response_logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "context" 12 | 13 | tq "github.com/facebookincubator/tacquito" 14 | ) 15 | 16 | // NewResponseLogger will wrap another handler as middleware. Next is the actual handler 17 | // that will be called by the server. 18 | func NewResponseLogger(ctx context.Context, l loggerProvider, next tq.Handler) *ResponseLogger { 19 | return &ResponseLogger{ctx: ctx, loggerProvider: l, next: next} 20 | } 21 | 22 | // ResponseLogger is a middleware handler that logs responses from the server 23 | type ResponseLogger struct { 24 | ctx context.Context 25 | loggerProvider 26 | next tq.Handler 27 | } 28 | 29 | // Write response fields to logger 30 | func (l *ResponseLogger) Write(ctx context.Context, p []byte) (int, error) { 31 | packet := tq.NewPacket() 32 | err := packet.UnmarshalBinary(p) 33 | if err != nil { 34 | return 0, err 35 | } 36 | request := tq.Request{Header: *packet.Header, Body: packet.Body[:], Context: ctx} 37 | l.Record(ctx, request.Fields(tq.ContextConnRemoteAddr, tq.ContextConnLocalAddr, tq.ContextUser, tq.ContextRemoteAddr, tq.ContextReqArgs, tq.ContextAcctType, tq.ContextPrivLvl, tq.ContextPort, tq.ContextFlags)) 38 | 39 | return 0, nil 40 | } 41 | 42 | // Handle implements a middleware logger for next 43 | func (l *ResponseLogger) Handle(response tq.Response, request tq.Request) { 44 | // ResponseLogger's context should include all of contextual fields from the request 45 | // if the request's context was used to initialize the logger 46 | request.Context = l.ctx 47 | response.Context(l.ctx) 48 | response.RegisterWriter(l) 49 | l.next.Handle(response, request) 50 | } 51 | 52 | // recorder is a private interface for the handlers package. 53 | // it lets us abstract an object which can be used to store persistent data 54 | // also used to intercept the handler state machine. the interface would help 55 | // to make this dependency be injectable from main in the future 56 | type recorderWriter interface { 57 | RecordCtx(request *tq.Request, keys ...tq.ContextKey) 58 | Context() context.Context 59 | Write(ctx context.Context, p []byte) (int, error) 60 | } 61 | 62 | // ctxLogger is a middleware handler that logs responses from the server 63 | type ctxLogger struct { 64 | loggerProvider 65 | tq.Writer 66 | ctx context.Context 67 | } 68 | 69 | // newPacketLogger is a logger scoped to a AAA handler's lifetime 70 | func newPacketLogger(l loggerProvider) *ctxLogger { 71 | return &ctxLogger{loggerProvider: l, Writer: &ResponseLogger{loggerProvider: l}} 72 | } 73 | 74 | // Context returns the ctxLogger's stored context 75 | // This context could be nil if this method is called before RecordCtx 76 | func (cl *ctxLogger) Context() context.Context { 77 | return cl.ctx 78 | } 79 | 80 | // RecordCtx receives a request object, and a set of context keys 81 | // it will call the loggerProvider's Set function to process context keys 82 | // which store data that is supposed to be persistent for a handler's lifetime 83 | func (cl *ctxLogger) RecordCtx(request *tq.Request, keys ...tq.ContextKey) { 84 | if cl.ctx == nil { 85 | cl.ctx = request.Context 86 | } 87 | cl.ctx = cl.Set(cl.ctx, request.Fields(), keys...) 88 | } 89 | -------------------------------------------------------------------------------- /cmds/server/handlers/span.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "net" 14 | "strings" 15 | "time" 16 | 17 | tq "github.com/facebookincubator/tacquito" 18 | "github.com/facebookincubator/tacquito/cmds/server/config" 19 | 20 | "github.com/prometheus/client_golang/prometheus" 21 | ) 22 | 23 | const ( 24 | // this is the tcp connection idle timeout. It will act as a initial deadline on the 25 | // tcp conn, and the conn Write deadline is reset to this value on every successful write 26 | idleTimeout = 5 * time.Second 27 | ) 28 | 29 | // NewSpan ... 30 | func NewSpan(l loggerProvider) *Span { 31 | return &Span{loggerProvider: l} 32 | } 33 | 34 | // Span is the main entry point for incoming aaa messages from clients. 35 | type Span struct { 36 | loggerProvider 37 | configProvider 38 | ctx context.Context 39 | destination string 40 | switchAddr string 41 | remAddr string 42 | packetType tq.HeaderType 43 | } 44 | 45 | func strToHeaderType(packetType string) tq.HeaderType { 46 | packetType = strings.ToLower(packetType) 47 | switch packetType { 48 | case "authenticate": 49 | return tq.Authenticate 50 | case "authorize": 51 | return tq.Authorize 52 | case "accounting": 53 | return tq.Accounting 54 | } 55 | return 0 56 | } 57 | 58 | // New ... 59 | func (s *Span) New(ctx context.Context, c config.Provider, options map[string]string) tq.Handler { 60 | destination, ok := options["destination"] 61 | if !ok { 62 | s.Errorf(ctx, "Unable to find key destination in handler options") 63 | return nil 64 | } 65 | return &Span{ 66 | loggerProvider: s.loggerProvider, 67 | ctx: ctx, 68 | configProvider: c, destination: destination, 69 | switchAddr: options["switchAddr"], 70 | remAddr: options["remAddr"], 71 | packetType: strToHeaderType(options["packetType"]), 72 | } 73 | } 74 | 75 | type writer struct { 76 | loggerProvider 77 | net.Conn 78 | ctx context.Context 79 | switchAddr string 80 | remAddr string 81 | packetType tq.HeaderType 82 | } 83 | 84 | // Write sends the req/response from client/server to span host 85 | // after filtering on fields inside the packet 86 | // currently supported fields are rem-addr(remote-host), switchAddr(switch to which user is trying to login to) 87 | // and packet-Type (authenticate/authorise/accounting) 88 | func (w writer) Write(ctx context.Context, p []byte) (int, error) { 89 | if w.Conn == nil { 90 | spanHandleWriteError.Inc() 91 | w.Errorf(w.ctx, "connection object attached to writer is invalid") 92 | return 0, fmt.Errorf("inactive connection object") 93 | } 94 | remoteAddr := w.RemoteAddr().String() 95 | if w.switchAddr != "" && remoteAddr != w.switchAddr { 96 | spanHandleWriteError.Inc() 97 | s := fmt.Sprintf("Skipping packet, switchAddr don't match, actual addr %v vs configured addr %v", remoteAddr, w.switchAddr) 98 | w.Errorf(w.ctx, s) 99 | return 0, fmt.Errorf(s) 100 | } 101 | packet := tq.NewPacket() 102 | packet.UnmarshalBinary(p) 103 | if w.packetType != 0 && packet.Header.Type != w.packetType { 104 | spanHandleWriteError.Inc() 105 | s := fmt.Sprintf("Skipping packet, Packet types don't match, actual type %v vs configured type %v", packet.Header.Type, w.packetType) 106 | w.Errorf(w.ctx, s) 107 | return 0, fmt.Errorf(s) 108 | } 109 | if w.remAddr != "" { 110 | req := tq.Request{Header: *packet.Header, Body: packet.Body[:]} 111 | fields := req.Fields() 112 | remAddrField, found := fields["rem-addr"] 113 | if found && remAddrField != w.remAddr { 114 | spanHandleWriteError.Inc() 115 | s := fmt.Sprintf("Skipping packet, client IPs don't match, actual client IP %v vs configured IP %v", remAddrField, w.remAddr) 116 | w.Errorf(w.ctx, s) 117 | return 0, fmt.Errorf(s) 118 | } 119 | } 120 | n, err := w.Conn.Write(p) 121 | if err != nil { 122 | spanHandleWriteError.Inc() 123 | return n, err 124 | } 125 | // successful write, let's increase the idletimeout 126 | w.Infof(w.ctx, "Wrote %v bytes to connection", n) 127 | w.SetWriteDeadline(time.Now().Add(idleTimeout)) 128 | spanHandleWriteSuccess.Inc() 129 | return n, err 130 | } 131 | 132 | func (s *Span) dialHost() (net.Conn, error) { 133 | c, err := net.Dial("tcp6", s.destination) 134 | if err != nil { 135 | return nil, fmt.Errorf("couldn't dial the connection to %v due to error %v", s.destination, err) 136 | } 137 | s.Infof(s.ctx, "Dialled a tcp connection to host %v", s.destination) 138 | return c, nil 139 | } 140 | 141 | // Handle ... 142 | func (s *Span) Handle(response tq.Response, request tq.Request) { 143 | spanHandle.Inc() 144 | timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { 145 | ms := v * 1000 // make milliseconds 146 | spanDurations.Observe(ms) 147 | })) 148 | start := time.Now() 149 | conn, err := s.dialHost() 150 | callNextHandler := func() { 151 | nextHandler := NewStart(s.loggerProvider).New(request.Context, s.configProvider.(config.Provider), nil) 152 | nextHandler.Handle(response, request) 153 | } 154 | if err != nil { 155 | spanHandleError.Inc() 156 | s.Errorf(request.Context, "Unable to span connection due to error %v", err) 157 | callNextHandler() 158 | return 159 | } 160 | conn.SetWriteDeadline(time.Now().Add(idleTimeout)) 161 | w := &writer{loggerProvider: s.loggerProvider, 162 | Conn: conn, 163 | ctx: request.Context, 164 | remAddr: s.remAddr, 165 | switchAddr: s.switchAddr, 166 | packetType: s.packetType, 167 | } 168 | // Write the request to the connection 169 | req := tq.Packet{ 170 | Header: &request.Header, 171 | Body: request.Body[:], 172 | } 173 | reqBytes, err := req.MarshalBinary() 174 | if err != nil { 175 | s.Infof(request.Context, "unable to write request to connection due to error %v. Skipping packet...", err) 176 | callNextHandler() 177 | return 178 | } 179 | w.Write(request.Context, reqBytes) 180 | // Write responses 181 | go func() { 182 | for range request.Context.Done() { 183 | duration := time.Since(start) 184 | timer.ObserveDuration() 185 | s.Infof(request.Context, "Request context cancelled, total duration of connection %v", duration) 186 | w.Close() 187 | return 188 | } 189 | }() 190 | response.RegisterWriter(w) 191 | callNextHandler() 192 | } 193 | -------------------------------------------------------------------------------- /cmds/server/handlers/start.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package handlers 9 | 10 | import ( 11 | "context" 12 | tq "github.com/facebookincubator/tacquito" 13 | "github.com/facebookincubator/tacquito/cmds/server/config" 14 | ) 15 | 16 | // NewStart ... 17 | func NewStart(l loggerProvider) *Start { 18 | return &Start{loggerProvider: l} 19 | } 20 | 21 | // Start is the main entry point for incoming aaa messages from clients. 22 | type Start struct { 23 | loggerProvider 24 | configProvider 25 | options map[string]string 26 | } 27 | 28 | // New creates a new start handler. 29 | func (s *Start) New(ctx context.Context, c config.Provider, options map[string]string) tq.Handler { 30 | return &Start{loggerProvider: s.loggerProvider, configProvider: c} 31 | } 32 | 33 | // Handle implements the tq handler interface 34 | func (s *Start) Handle(response tq.Response, request tq.Request) { 35 | switch request.Header.Type { 36 | case tq.Authenticate: 37 | startAuthenticate.Inc() 38 | NewAuthenticateStart(s.loggerProvider, s.configProvider).Handle(response, request) 39 | case tq.Authorize: 40 | startAuthorize.Inc() 41 | NewAuthorizeRequest(s.loggerProvider, s.configProvider).Handle(response, request) 42 | case tq.Accounting: 43 | startAccounting.Inc() 44 | NewAccountingRequest(s.loggerProvider, s.configProvider).Handle(response, request) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmds/server/loader/fsnotify/fsnotify.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package fsnotify 9 | 10 | // Package fsnotify implements the inotify equivalent for the loader types using fsnotify. 11 | // fsnotify should be used to detect changes in config files and hanlde dynamically loading them 12 | 13 | import ( 14 | "context" 15 | "fmt" 16 | "path/filepath" 17 | "strings" 18 | "time" 19 | 20 | "github.com/facebookincubator/tacquito/cmds/server/config" 21 | "github.com/fsnotify/fsnotify" 22 | ) 23 | 24 | type loader interface { 25 | Load(path string) error 26 | Config() chan config.ServerConfig 27 | } 28 | 29 | type loggerProvider interface { 30 | Infof(ctx context.Context, format string, args ...interface{}) 31 | Errorf(ctx context.Context, format string, args ...interface{}) 32 | Debugf(ctx context.Context, format string, args ...interface{}) 33 | } 34 | 35 | // Watcher is a type that waches for config changes and processes config updates 36 | // Watcher really just wraps other Loader types 37 | type Watcher struct { 38 | loader 39 | loggerProvider 40 | ctx context.Context 41 | watchman *fsnotify.Watcher 42 | config chan config.ServerConfig 43 | } 44 | 45 | // New ... 46 | func New(ctx context.Context, l loader, logger loggerProvider) *Watcher { 47 | return &Watcher{ctx: ctx, loader: l, loggerProvider: logger, config: make(chan config.ServerConfig, 1)} 48 | } 49 | 50 | // Load ... 51 | func (w *Watcher) Load(path string) error { 52 | if err := w.loader.Load(path); err != nil { 53 | return fmt.Errorf("loader failed: %v", err) 54 | } 55 | 56 | watcher, err := fsnotify.NewWatcher() 57 | if err != nil { 58 | return fmt.Errorf("failed to create file watcher: %v", err) 59 | } 60 | if err := watcher.Add(filepath.Dir(path)); err != nil { 61 | return fmt.Errorf("failed watching config: %s", err) 62 | } 63 | w.watchman = watcher 64 | go w.watch(path) 65 | return nil 66 | } 67 | 68 | // watch ... 69 | // You only want to call this ONCE 70 | func (w *Watcher) watch(path string) { 71 | base := filepath.Base(path) 72 | w.Infof(w.ctx, "watching %s", base) 73 | ticker := time.NewTicker(time.Second * 1) 74 | var pending int 75 | for { 76 | select { 77 | case <-w.ctx.Done(): 78 | w.Infof(w.ctx, "exiting watch loop for fsnotify; %v", w.ctx.Err()) 79 | return 80 | case ev := <-w.watchman.Events: 81 | if ev.Op&fsnotify.Write == fsnotify.Write { 82 | // fsnotify monitors the entire directory of the config file 83 | // this check ignores things that aren't the config file 84 | // also ignores the .config.swp file to reduce noise 85 | if !strings.Contains(ev.String(), base) || filepath.Ext(ev.Name) == ".swp" { 86 | w.Debugf(w.ctx, "not the config file, skipping event %v", ev) 87 | ticker.Reset(time.Second * 1) 88 | continue 89 | } 90 | w.Debugf(w.ctx, "config file changed from event %v", ev) 91 | pending++ //track num of changes 92 | } 93 | case err := <-w.watchman.Errors: 94 | w.Errorf(w.ctx, "Error: ", err) 95 | case <-ticker.C: 96 | if pending > 0 { 97 | pending = 0 98 | w.Infof(w.ctx, "reloading config [%v]", path) 99 | if err := w.loader.Load(path); err != nil { 100 | w.Errorf(w.ctx, "bad config for path [%v]: %v", path, err) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | // Config ... 108 | func (w *Watcher) Config() chan config.ServerConfig { 109 | return w.loader.Config() 110 | } 111 | -------------------------------------------------------------------------------- /cmds/server/loader/json/json.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package json 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "io" 14 | "os" 15 | 16 | "github.com/facebookincubator/tacquito/cmds/server/config" 17 | ) 18 | 19 | // New returns a new yaml config unmarshaller 20 | func New() *JSON { 21 | // TODO move channel to inotify 22 | return &JSON{config: make(chan config.ServerConfig, 1)} 23 | } 24 | 25 | // JSON loads all users from a given config filename 26 | type JSON struct { 27 | config.ServerConfig 28 | config chan config.ServerConfig 29 | } 30 | 31 | // Load given a filename from disk, read all user data from it and unmarshal it 32 | func (l *JSON) Load(path string) error { 33 | f, err := os.Open(path) 34 | if err != nil { 35 | return err 36 | } 37 | b, err := io.ReadAll(f) 38 | if err != nil { 39 | return err 40 | } 41 | return l.Unmarshal(b) 42 | } 43 | 44 | // Unmarshal will decode bytes 45 | func (l *JSON) Unmarshal(b []byte) error { 46 | if err := json.Unmarshal(b, &l.ServerConfig); err != nil { 47 | return fmt.Errorf("unable to unmarshal server config; %v", err) 48 | } 49 | if len(l.ServerConfig.Secrets) < 1 { 50 | return fmt.Errorf("no secret providers were unmarshalled from config, cannot serve") 51 | } 52 | if len(l.ServerConfig.Users) < 1 { 53 | return fmt.Errorf("no users were unmarshalled from config, cannot serve") 54 | } 55 | l.config <- l.ServerConfig 56 | return nil 57 | } 58 | 59 | // Config must return a threadsafe copy of the underlying config. 60 | func (l JSON) Config() chan config.ServerConfig { 61 | return l.config 62 | } 63 | -------------------------------------------------------------------------------- /cmds/server/loader/prefix_filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package loader 9 | 10 | import ( 11 | "net" 12 | ) 13 | 14 | // newPrefixFilter creates a basic prefix filter for any incoming connections. If 15 | // this is provided to the server, we will never speak to any clients that do not 16 | // pass this check. This allows other providers to determine how to best interact 17 | // with a client and offloads some basic security checks 18 | func newPrefixFilter(prefixes []*net.IPNet) *prefixFilter { 19 | f := &prefixFilter{known: make(map[string]struct{})} 20 | for _, ipnet := range prefixes { 21 | f.known[ipnet.String()] = struct{}{} 22 | } 23 | return f 24 | } 25 | 26 | // prefixFilter holds a cache of prefixes we are allowed to speak to 27 | type prefixFilter struct { 28 | known map[string]struct{} 29 | } 30 | 31 | // match determines if we are matched to speak/not speak to a client's source prefix. If no 32 | // prefixes are provided, we fail open. 33 | func (p prefixFilter) match(addr *net.TCPAddr) bool { 34 | for cidr := range p.known { 35 | _, ipNet, _ := net.ParseCIDR(cidr) 36 | if ipNet != nil && ipNet.Contains(addr.IP) { 37 | return true 38 | } 39 | } 40 | return false 41 | } 42 | 43 | // deny is our deny list 44 | func (p prefixFilter) deny(remote net.Addr) bool { 45 | if len(p.known) < 1 { 46 | return false 47 | } 48 | addr, ok := remote.(*net.TCPAddr) 49 | if !ok { 50 | prefixFilterDenied.Inc() 51 | return true 52 | } 53 | if p.match(addr) { 54 | prefixFilterDenied.Inc() 55 | return true 56 | } 57 | prefixFilterAllowed.Inc() 58 | return false 59 | } 60 | 61 | // allow is our allow list 62 | func (p prefixFilter) allow(remote net.Addr) bool { 63 | if len(p.known) < 1 { 64 | return true 65 | } 66 | addr, ok := remote.(*net.TCPAddr) 67 | if !ok { 68 | prefixFilterDenied.Inc() 69 | return false 70 | } 71 | if p.match(addr) { 72 | prefixFilterAllowed.Inc() 73 | return true 74 | } 75 | prefixFilterDenied.Inc() 76 | return false 77 | } 78 | -------------------------------------------------------------------------------- /cmds/server/loader/prefixfilter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package loader 9 | 10 | import ( 11 | "fmt" 12 | "net" 13 | "testing" 14 | 15 | "github.com/davecgh/go-spew/spew" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | // prefixBuilder converts cidr string values to *net.IPNet values, ignoring 20 | // invalid ones. 21 | func testPrefixBuilder(prefixes ...string) []*net.IPNet { 22 | allowed := make([]*net.IPNet, 0, len(prefixes)) 23 | for _, cidr := range prefixes { 24 | if _, ipNet, _ := net.ParseCIDR(cidr); ipNet != nil { 25 | allowed = append(allowed, ipNet) 26 | } 27 | } 28 | return allowed 29 | } 30 | 31 | func TestPrefixAllow(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | allowed []*net.IPNet 35 | remote net.Addr 36 | expected bool 37 | }{ 38 | { 39 | name: "test1", 40 | allowed: testPrefixBuilder("2401:db00::/64"), 41 | remote: &net.TCPAddr{IP: net.ParseIP("2001:db8::68")}, 42 | expected: false, 43 | }, 44 | { 45 | name: "test2", 46 | allowed: testPrefixBuilder("2401:db00::/64"), 47 | remote: &net.TCPAddr{IP: net.ParseIP("2401:db00::")}, 48 | expected: true, 49 | }, 50 | { 51 | name: "test3", 52 | allowed: testPrefixBuilder("2401:db00::/64"), 53 | remote: &net.TCPAddr{IP: net.ParseIP("2401:db00::1")}, 54 | expected: true, 55 | }, 56 | { 57 | name: "test4 - fail open if nothing provided to filter", 58 | allowed: nil, 59 | remote: &net.TCPAddr{IP: net.ParseIP("2401:db00::1")}, 60 | expected: true, 61 | }, 62 | } 63 | for _, test := range tests { 64 | f := newPrefixFilter(test.allowed) 65 | spew.Dump(test) 66 | assert.Equal(t, test.expected, f.allow(test.remote), fmt.Sprintf("failed %v", test.name)) 67 | } 68 | } 69 | 70 | func TestPrefixDeny(t *testing.T) { 71 | tests := []struct { 72 | name string 73 | deny []*net.IPNet 74 | remote net.Addr 75 | expected bool 76 | }{ 77 | { 78 | name: "test1", 79 | deny: testPrefixBuilder("2401:db00::/64"), 80 | remote: &net.TCPAddr{IP: net.ParseIP("2001:db8::68")}, 81 | expected: false, 82 | }, 83 | { 84 | name: "test2", 85 | deny: testPrefixBuilder("2401:db00::/64"), 86 | remote: &net.TCPAddr{IP: net.ParseIP("2401:db00::")}, 87 | expected: true, 88 | }, 89 | { 90 | name: "test3", 91 | deny: testPrefixBuilder("2401:db00::/64"), 92 | remote: &net.TCPAddr{IP: net.ParseIP("2402:db00::1")}, 93 | expected: false, 94 | }, 95 | { 96 | name: "test4 - fail open if nothing provided to filter", 97 | deny: nil, 98 | remote: &net.TCPAddr{IP: net.ParseIP("2401:db00::1")}, 99 | expected: false, 100 | }, 101 | } 102 | for _, test := range tests { 103 | f := newPrefixFilter(test.deny) 104 | spew.Dump(test) 105 | assert.Equal(t, test.expected, f.deny(test.remote), fmt.Sprintf("failed %v", test.name)) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cmds/server/loader/stats.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package loader 9 | 10 | import ( 11 | "github.com/prometheus/client_golang/prometheus" 12 | ) 13 | 14 | var ( 15 | secretKnown = prometheus.NewCounter(prometheus.CounterOpts{ 16 | Namespace: "tacquito", 17 | Name: "loader_get_secret_known", 18 | Help: "number of known secret providers in loader.get calls", 19 | }) 20 | secretUnknown = prometheus.NewCounter(prometheus.CounterOpts{ 21 | Namespace: "tacquito", 22 | Name: "loader_get_secret_unknown", 23 | Help: "number of unknown secret providers in loader.get calls", 24 | }) 25 | scope = prometheus.NewCounter(prometheus.CounterOpts{ 26 | Namespace: "tacquito", 27 | Name: "loader_build_scope", 28 | Help: "number of scopes processed", 29 | }) 30 | userScopeDuplicate = prometheus.NewCounter(prometheus.CounterOpts{ 31 | Namespace: "tacquito", 32 | Name: "loader_build_user_scope_duplicate", 33 | Help: "number of duplicate user scopes encountered", 34 | }) 35 | userAuthorizerUnassigned = prometheus.NewCounter(prometheus.CounterOpts{ 36 | Namespace: "tacquito", 37 | Name: "loader_build_user_authorizer_unassigned", 38 | Help: "number of user with unassigned authorizers", 39 | }) 40 | userAuthorizerBadConfigRef = prometheus.NewCounter(prometheus.CounterOpts{ 41 | Namespace: "tacquito", 42 | Name: "loader_build_user_authorizer_bad_configref", 43 | Help: "number of user with bad config ref authorizer", 44 | }) 45 | userAuthenticatorUnassigned = prometheus.NewCounter(prometheus.CounterOpts{ 46 | Namespace: "tacquito", 47 | Name: "loader_build_user_authenticator_unassigned", 48 | Help: "number of user with unassigned authenticators", 49 | }) 50 | userAuthenticatorBadConfigRef = prometheus.NewCounter(prometheus.CounterOpts{ 51 | Namespace: "tacquito", 52 | Name: "loader_build_user_authenticator_bad_configref", 53 | Help: "number of user with bad config ref authenticators", 54 | }) 55 | userAccounterUnassigned = prometheus.NewCounter(prometheus.CounterOpts{ 56 | Namespace: "tacquito", 57 | Name: "loader_build_user_accounter_unassigned", 58 | Help: "number of user with unassigned accounters", 59 | }) 60 | userAccounterBadConfigRef = prometheus.NewCounter(prometheus.CounterOpts{ 61 | Namespace: "tacquito", 62 | Name: "loader_build_user_accounter_bad_configref_error", 63 | Help: "number of user with bad config ref accounters", 64 | }) 65 | userTotal = prometheus.NewCounter(prometheus.CounterOpts{ 66 | Namespace: "tacquito", 67 | Name: "loader_build_user_total", 68 | Help: "number of users processed in a cycle", 69 | }) 70 | userScopeUnassigned = prometheus.NewCounter(prometheus.CounterOpts{ 71 | Namespace: "tacquito", 72 | Name: "loader_build_user_scope_unassigned", 73 | Help: "number of user scopes unassigned", 74 | }) 75 | secretProviderMissing = prometheus.NewCounter(prometheus.CounterOpts{ 76 | Namespace: "tacquito", 77 | Name: "loader_build_secret_provider_missing", 78 | Help: "number of missing secret providers", 79 | }) 80 | providerFactoryMissing = prometheus.NewCounter(prometheus.CounterOpts{ 81 | Namespace: "tacquito", 82 | Name: "loader_build_user_provider_factory_missing", 83 | Help: "number of missing user provider factory", 84 | }) 85 | secretProviderGet = prometheus.NewGauge(prometheus.GaugeOpts{ 86 | Namespace: "tacquito", 87 | Name: "loader_build_secret_provider_get", 88 | Help: "number of active secret provider queries", 89 | }) 90 | buildUpdate = prometheus.NewCounter(prometheus.CounterOpts{ 91 | Namespace: "tacquito", 92 | Name: "loader_update_build", 93 | Help: "number of builds on config updates", 94 | }) 95 | buildGet = prometheus.NewCounter(prometheus.CounterOpts{ 96 | Namespace: "tacquito", 97 | Name: "loader_update_get", 98 | Help: "number of config get calls from updates", 99 | }) 100 | userOverrideAuthenticator = prometheus.NewCounter(prometheus.CounterOpts{ 101 | Namespace: "tacquito", 102 | Name: "loader_loader_reduceAuthenticatorAccounterFromGroups_user_override_authenticator", 103 | Help: "number of user overrides for authenticator", 104 | }) 105 | userOverrideAccounter = prometheus.NewCounter(prometheus.CounterOpts{ 106 | Namespace: "tacquito", 107 | Name: "loader_loader_reduceAuthenticatorAccounterFromGroups_user_override_accounter", 108 | Help: "number of user overrides for accounter", 109 | }) 110 | prefixFilterAllowed = prometheus.NewCounter(prometheus.CounterOpts{ 111 | Namespace: "tacquito", 112 | Name: "prefixFilter_allowed", 113 | Help: "when prefixFilter allows a remote net.Addr, this is incremented", 114 | }) 115 | prefixFilterDenied = prometheus.NewCounter(prometheus.CounterOpts{ 116 | Namespace: "tacquito", 117 | Name: "prefixFilter_denied", 118 | Help: "when prefixFilter denies a remote net.Addr, this is incremented", 119 | }) 120 | 121 | // Durations 122 | buildDuration = prometheus.NewSummary(prometheus.SummaryOpts{ 123 | Namespace: "tacquito", 124 | Name: "loader_build_duration", 125 | Help: "duration of a successful config build in milliseconds", 126 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, 127 | }) 128 | ) 129 | 130 | func init() { 131 | prometheus.MustRegister(secretKnown) 132 | prometheus.MustRegister(secretUnknown) 133 | prometheus.MustRegister(scope) 134 | prometheus.MustRegister(userScopeDuplicate) 135 | prometheus.MustRegister(userAuthorizerUnassigned) 136 | prometheus.MustRegister(userAuthorizerBadConfigRef) 137 | prometheus.MustRegister(userAuthenticatorUnassigned) 138 | prometheus.MustRegister(userAuthenticatorBadConfigRef) 139 | prometheus.MustRegister(userAccounterUnassigned) 140 | prometheus.MustRegister(userAccounterBadConfigRef) 141 | prometheus.MustRegister(userTotal) 142 | prometheus.MustRegister(userScopeUnassigned) 143 | prometheus.MustRegister(secretProviderMissing) 144 | prometheus.MustRegister(providerFactoryMissing) 145 | prometheus.MustRegister(secretProviderGet) 146 | prometheus.MustRegister(buildUpdate) 147 | prometheus.MustRegister(buildGet) 148 | prometheus.MustRegister(userOverrideAuthenticator) 149 | prometheus.MustRegister(userOverrideAccounter) 150 | prometheus.MustRegister(prefixFilterAllowed) 151 | prometheus.MustRegister(prefixFilterDenied) 152 | 153 | // Durations 154 | prometheus.MustRegister(buildDuration) 155 | } 156 | -------------------------------------------------------------------------------- /cmds/server/loader/testdata/test_config.yaml: -------------------------------------------------------------------------------- 1 | # resuable references 2 | authenticator_type_bcrypt: &authenticator_type_bcrypt 1 3 | 4 | privlvl_root: &privlvl_root 15 5 | 6 | action_deny: &action_deny 1 7 | action_permit: &action_permit 2 8 | 9 | logger_type_stderr: &logger_type_stderr 1 10 | logger_type_syslog: &logger_type_syslog 2 11 | 12 | logger_stderr: &logger_stderr 13 | name: stderr 14 | type: *logger_type_stderr 15 | options: 16 | foo: bar 17 | 18 | logger_syslog: &logger_syslog 19 | name: syslog 20 | type: *logger_type_syslog 21 | options: 22 | facility: user 23 | severity: informational 24 | 25 | bcrypt: &bcrypt 26 | type: *authenticator_type_bcrypt 27 | options: 28 | keychain: tacquito 29 | key: password 30 | 31 | # services 32 | enable: &enable 33 | name: enable 34 | set_values: 35 | - name: priv-lvl 36 | values: [ *privlvl_root ] 37 | 38 | # commands 39 | conf_t: &conf_t 40 | name: configure 41 | match: [terminal, exclusive] 42 | action: *action_permit 43 | 44 | conf_b: &conf_b 45 | name: configure 46 | match: [batch] 47 | action: *action_permit 48 | 49 | # groups 50 | noc: &noc 51 | name: noc 52 | services: [*enable] 53 | commands: [*conf_t, *conf_b] 54 | authenticator: *bcrypt 55 | accounter: *logger_stderr 56 | 57 | 58 | # finally, declare users 59 | users: 60 | - name: mr_uses_group 61 | scopes: ["localhost"] 62 | groups: [*noc] 63 | - name: mr_no_group 64 | scopes: ["localhost"] 65 | services: [*enable] 66 | commands: [*conf_t] 67 | authenticator: *bcrypt 68 | accounter: *logger_stderr 69 | - name: ms_commands_only 70 | scopes: ["localhost"] 71 | commands: [*conf_t] 72 | 73 | 74 | handler_type_start: &handler_type_start 1 75 | handler_type_span: &handler_type_span 2 76 | 77 | provider_type_prefix: &provider_type_prefix 1 78 | provider_type_dns: &provider_type_dns 2 79 | 80 | secrets: 81 | - name: localhost 82 | secret: 83 | group: tacquito 84 | key: fooman 85 | handler: 86 | type: *handler_type_start 87 | type: *provider_type_prefix 88 | options: 89 | prefixes: | 90 | [ 91 | "::0/0" 92 | ] 93 | -------------------------------------------------------------------------------- /cmds/server/loader/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package yaml 9 | 10 | import ( 11 | "fmt" 12 | "io" 13 | "os" 14 | 15 | "github.com/facebookincubator/tacquito/cmds/server/config" 16 | 17 | "gopkg.in/yaml.v3" 18 | ) 19 | 20 | // New returns a new yaml config unmarshaller 21 | func New() *YAML { 22 | // TODO move channel to inotify 23 | return &YAML{config: make(chan config.ServerConfig, 1)} 24 | } 25 | 26 | // YAML loads all users from a given config filename 27 | type YAML struct { 28 | config.ServerConfig 29 | config chan config.ServerConfig 30 | } 31 | 32 | // Load given a filename from disk, read all user data from it and unmarshal it 33 | func (l *YAML) Load(path string) error { 34 | f, err := os.Open(path) 35 | if err != nil { 36 | return fmt.Errorf("failed to open file: %v", err) 37 | } 38 | b, err := io.ReadAll(f) 39 | if err != nil { 40 | return fmt.Errorf("failed to read file: %v", err) 41 | } 42 | 43 | return l.Unmarshal(b) 44 | } 45 | 46 | // Unmarshal will decode bytes 47 | func (l *YAML) Unmarshal(b []byte) error { 48 | if err := yaml.Unmarshal(b, &l.ServerConfig); err != nil { 49 | return fmt.Errorf("unable to unmarshal server config; %v", err) 50 | } 51 | if len(l.Secrets) < 1 { 52 | return fmt.Errorf("no secret providers were unmarshalled from config, cannot serve") 53 | } 54 | if len(l.Users) < 1 { 55 | return fmt.Errorf("no users were unmarshalled from config, cannot serve") 56 | } 57 | l.config <- l.ServerConfig 58 | return nil 59 | } 60 | 61 | // Config must return a threadsafe copy of the underlying config. 62 | func (l YAML) Config() chan config.ServerConfig { 63 | return l.config 64 | } 65 | -------------------------------------------------------------------------------- /cmds/server/loader/yaml_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package loader 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/facebookincubator/tacquito/cmds/server/config" 14 | "github.com/facebookincubator/tacquito/cmds/server/loader/yaml" 15 | 16 | "github.com/davecgh/go-spew/spew" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func TestYamlLoad(t *testing.T) { 21 | l := yaml.New() 22 | go func() { 23 | err := l.Load("./testdata/test_config.yaml") 24 | assert.NoError(t, err) 25 | }() 26 | // if you get a bad config parse error, this will block because of how buck hides stderr/out 27 | <-l.Config() 28 | spew.Dump(l) 29 | 30 | bcrypt := &config.Authenticator{ 31 | Type: config.BCRYPT, 32 | Options: map[string]string{"keychain": "tacquito", "key": "password"}, 33 | } 34 | stderr := &config.Accounter{ 35 | Name: "stderr", 36 | Type: config.STDERR, 37 | Options: map[string]string{"foo": "bar"}, 38 | } 39 | conft := config.Command{Name: "configure", Match: []string{"terminal", "exclusive"}, Action: config.PERMIT} 40 | confb := config.Command{Name: "configure", Match: []string{"batch"}, Action: config.PERMIT} 41 | noc := config.Group{ 42 | Name: "noc", 43 | Services: []config.Service{ 44 | { 45 | Name: "enable", 46 | SetValues: []config.Value{ 47 | {Name: "priv-lvl", Values: []string{"15"}}, 48 | }, 49 | }, 50 | }, 51 | Commands: []config.Command{conft, confb}, 52 | Authenticator: bcrypt, 53 | Accounter: stderr, 54 | } 55 | 56 | users := []config.User{ 57 | { 58 | Name: "mr_uses_group", 59 | Scopes: []string{"localhost"}, 60 | Groups: []config.Group{noc}, 61 | }, 62 | { 63 | Name: "mr_no_group", 64 | Scopes: []string{"localhost"}, 65 | Services: []config.Service{ 66 | { 67 | Name: "enable", 68 | SetValues: []config.Value{ 69 | {Name: "priv-lvl", Values: []string{"15"}}, 70 | }, 71 | }, 72 | }, 73 | Commands: []config.Command{conft}, 74 | Authenticator: bcrypt, 75 | Accounter: stderr, 76 | }, 77 | { 78 | Name: "ms_commands_only", 79 | Scopes: []string{"localhost"}, 80 | Commands: []config.Command{conft}, 81 | }, 82 | } 83 | 84 | expected := yaml.YAML{ 85 | ServerConfig: config.ServerConfig{ 86 | Users: users, 87 | Secrets: []config.SecretConfig{ 88 | { 89 | Name: "localhost", 90 | Secret: config.Keychain{Group: "tacquito", Key: "fooman"}, 91 | Handler: config.Handler{Type: config.START}, 92 | Type: config.PREFIX, 93 | Options: map[string]string{ 94 | "prefixes": "[\n \"::0/0\"\n]\n", 95 | }, 96 | }, 97 | }, 98 | }, 99 | } 100 | assert.Equal(t, expected.Users, l.Users) 101 | assert.Equal(t, expected.Secrets, l.Secrets) 102 | } 103 | -------------------------------------------------------------------------------- /cmds/server/log/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package log 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "io" 14 | "log" 15 | 16 | tq "github.com/facebookincubator/tacquito" 17 | ) 18 | 19 | // New provides a basic logger if one is not provided 20 | // levels: 10 error, 20 info, 30 debug. fatal has no level 21 | func New(level int, w io.Writer) *Logger { 22 | base := log.New(w, "", 0) 23 | meta := log.Ldate | log.Ltime | log.Llongfile 24 | return &Logger{ 25 | level: level, 26 | errorLogger: log.New(base.Writer(), "ERROR: ", meta), 27 | infoLogger: log.New(base.Writer(), "INFO: ", meta), 28 | debugLogger: log.New(base.Writer(), "DEBUG: ", meta), 29 | fatalLogger: log.New(base.Writer(), "FATAL: ", meta), 30 | } 31 | } 32 | 33 | // Logger ... 34 | type Logger struct { 35 | // log level to use 36 | level int 37 | // errorLogger is Level Error Logger 38 | errorLogger *log.Logger 39 | // infoLogger is Level Info Logger 40 | infoLogger *log.Logger 41 | // debugLogger is a Level Debug Logger 42 | debugLogger *log.Logger 43 | // fatalLogger is a Level Fatal Logger 44 | fatalLogger *log.Logger 45 | } 46 | 47 | // Record provides a log hook for record based log formats. errors will be caught and logged to errorf 48 | func (d Logger) Record(ctx context.Context, r map[string]string, obscure ...string) { 49 | // hide fields as needed 50 | for _, key := range obscure { 51 | if _, ok := r[key]; ok { 52 | r[key] = "" 53 | } 54 | } 55 | // do you own thing here 56 | d.Debugf(ctx, "%v", r) 57 | } 58 | 59 | // Errorf ... 60 | func (d Logger) Errorf(ctx context.Context, format string, args ...interface{}) { 61 | if d.level >= 10 { 62 | d.errorLogger.Output(2, fmt.Sprintf(format, args...)) 63 | } 64 | } 65 | 66 | // Infof ... 67 | func (d Logger) Infof(ctx context.Context, format string, args ...interface{}) { 68 | if d.level >= 20 { 69 | d.infoLogger.Output(2, fmt.Sprintf(format, args...)) 70 | } 71 | } 72 | 73 | // Debugf ... 74 | func (d Logger) Debugf(ctx context.Context, format string, args ...interface{}) { 75 | if d.level >= 30 { 76 | d.debugLogger.Output(2, fmt.Sprintf(format, args...)) 77 | } 78 | } 79 | 80 | // Fatalf ... 81 | func (d Logger) Fatalf(ctx context.Context, format string, args ...interface{}) { 82 | d.fatalLogger.Output(2, fmt.Sprintf(format, args...)) 83 | } 84 | 85 | // Set will extract keys from the request, and save them to the 86 | // logger's context 87 | func (d Logger) Set(ctx context.Context, fields map[string]string, keys ...tq.ContextKey) context.Context { 88 | // set fields here if needed 89 | // for _, key := range keys { 90 | // ctx = context.WithValue(ctx, key, fields[string(key)]) 91 | // } 92 | return ctx 93 | } 94 | -------------------------------------------------------------------------------- /cmds/server/log/log_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package log 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "io" 14 | "testing" 15 | 16 | "github.com/davecgh/go-spew/spew" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | // benchTest is used for allocation testing 21 | type benchTest struct { 22 | name string 23 | fn func(b *testing.B) 24 | expected func(name string, r testing.BenchmarkResult) 25 | } 26 | 27 | // TestLog0 benchmarks allocations for our logger 28 | func TestLog0Allocation(t *testing.T) { 29 | tests := []benchTest{ 30 | { 31 | name: "BenchmarkLog0", 32 | fn: BenchmarkLog0, 33 | expected: func(name string, r testing.BenchmarkResult) { 34 | t.Log(spew.Sdump(r)) 35 | expectedAllocs := 0 36 | actual := r.AllocsPerOp() 37 | assert.EqualValues(t, expectedAllocs, actual, fmt.Sprintf("%s allocations were not nominal; wanted %v got %v", name, expectedAllocs, actual)) 38 | }, 39 | }, 40 | { 41 | name: "BenchmarkLog10", 42 | fn: BenchmarkLog10, 43 | expected: func(name string, r testing.BenchmarkResult) { 44 | t.Log(spew.Sdump(r)) 45 | expectedAllocs := 1 46 | actual := r.AllocsPerOp() 47 | assert.EqualValues(t, expectedAllocs, actual, fmt.Sprintf("%s allocations were not nominal; wanted %v got %v", name, expectedAllocs, actual)) 48 | }, 49 | }, 50 | { 51 | name: "BenchmarkLog10Variadic", 52 | fn: BenchmarkLog10Variadic, 53 | expected: func(name string, r testing.BenchmarkResult) { 54 | t.Log(spew.Sdump(r)) 55 | expectedAllocs := 1 56 | actual := r.AllocsPerOp() 57 | assert.EqualValues(t, expectedAllocs, actual, fmt.Sprintf("%s allocations were not nominal; wanted %v got %v", name, expectedAllocs, actual)) 58 | }, 59 | }, 60 | } 61 | for _, test := range tests { 62 | r := testing.Benchmark(test.fn) 63 | test.expected(test.name, r) 64 | } 65 | } 66 | 67 | func BenchmarkLog0(b *testing.B) { 68 | logger := New(0, io.Discard) 69 | ctx := context.Background() 70 | // record allocations regardless of go test -test.bench 71 | b.ReportAllocs() 72 | for n := 0; n < b.N; n++ { 73 | logger.Errorf(ctx, "i am %s", "fooman") 74 | } 75 | } 76 | 77 | func BenchmarkLog10(b *testing.B) { 78 | logger := New(10, io.Discard) 79 | ctx := context.Background() 80 | // record allocations regardless of go test -test.bench 81 | b.ReportAllocs() 82 | for n := 0; n < b.N; n++ { 83 | logger.Errorf(ctx, "i am %s", "fooman") 84 | } 85 | } 86 | 87 | func BenchmarkLog10Variadic(b *testing.B) { 88 | logger := New(10, io.Discard) 89 | ctx := context.Background() 90 | // record allocations regardless of go test -test.bench 91 | b.ReportAllocs() 92 | for n := 0; n < b.N; n++ { 93 | logger.Errorf(ctx, "i am %s %s %s %s %s", "fooman", "but", "i", "am", "more") 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /cmds/server/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "context" 12 | 13 | "flag" 14 | "net" 15 | "os" 16 | "os/signal" 17 | 18 | tq "github.com/facebookincubator/tacquito" 19 | "github.com/facebookincubator/tacquito/cmds/server/config" 20 | "github.com/facebookincubator/tacquito/cmds/server/config/accounters/local" 21 | "github.com/facebookincubator/tacquito/cmds/server/config/authenticators/bcrypt" 22 | "github.com/facebookincubator/tacquito/cmds/server/config/authorizers/stringy" 23 | "github.com/facebookincubator/tacquito/cmds/server/log" 24 | 25 | "github.com/facebookincubator/tacquito/cmds/server/config/secret" 26 | "github.com/facebookincubator/tacquito/cmds/server/config/secret/prefix" 27 | "github.com/facebookincubator/tacquito/cmds/server/exporter" 28 | "github.com/facebookincubator/tacquito/cmds/server/handlers" 29 | "github.com/facebookincubator/tacquito/cmds/server/loader" 30 | "github.com/facebookincubator/tacquito/cmds/server/loader/fsnotify" 31 | "github.com/facebookincubator/tacquito/cmds/server/loader/yaml" 32 | ) 33 | 34 | var ( 35 | network = flag.String("network", "tcp6", "listen on tcp or tcp6") 36 | address = flag.String("address", ":2046", "listen on the provided address:port") 37 | proxy = flag.Bool("proxy", false, "proxy enables proxy header processing") 38 | configPath = flag.String("config", "tacquito.yaml", "the string path representing the storage location of the server config") 39 | accountingLogPath = flag.String("acct-log-path", "/tmp/tacquito_accounting.log", "the string path representing the storage location of the server accounting logs") 40 | level = flag.Int("level", 30, "log levels; 10 = error, 20 = info, 30 = debug") 41 | ) 42 | 43 | func main() { 44 | flag.Parse() 45 | logger := log.New(*level, os.Stderr) 46 | 47 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 48 | defer stop() 49 | 50 | ctx, cancel := context.WithCancel(ctx) 51 | defer cancel() 52 | 53 | // we need thrift running to collect Prometheus stats for ODS 54 | go func() { 55 | defer cancel() 56 | if err := exporter.StartPromHTTP(); err != nil { 57 | logger.Errorf(ctx, "failed to start prometheus http exporter: %v", err) 58 | } 59 | }() 60 | 61 | accountingLogger, err := local.New(logger, local.SetLogSinkDefault(*accountingLogPath, "tacquito")) 62 | if err != nil { 63 | logger.Fatalf(ctx, "error building accounting logger; %v", err) 64 | return 65 | } 66 | 67 | shhh := &shh{} 68 | sp, err := loader.NewLocalConfig( 69 | ctx, 70 | *configPath, 71 | fsnotify.New(ctx, yaml.New(), logger), 72 | loader.SetLoggerProvider(logger), 73 | loader.SetKeychainProvider(secret.New()), 74 | loader.SetConfigProvider(config.New()), 75 | loader.SetAuthorizerProvider(stringy.New(logger)), 76 | loader.RegisterSecretProviderType(config.PREFIX, prefix.New(logger)), 77 | loader.RegisterHandlerType(config.START, handlers.NewStart(logger)), 78 | loader.RegisterAuthenticator(config.BCRYPT, bcrypt.New(logger, shhh)), 79 | loader.RegisterAccounter(config.FILE, accountingLogger), 80 | ) 81 | if err != nil { 82 | logger.Fatalf(ctx, "error fetching config; %v", err) 83 | return 84 | } 85 | 86 | // setup our listener 87 | listener, err := net.Listen(*network, *address) 88 | if err != nil { 89 | logger.Fatalf(ctx, "error reading address: %v", err) 90 | return 91 | } 92 | 93 | tcpListener, ok := listener.(*net.TCPListener) 94 | if !ok { 95 | logger.Fatalf(ctx, "listener must be a tcp based listener") 96 | return 97 | } 98 | logger.Infof(ctx, "serve on %v", tcpListener.Addr().String()) 99 | 100 | s := tq.NewServer(logger, sp, tq.SetUseProxy(*proxy)) 101 | if err := s.Serve(ctx, tcpListener); err != nil { 102 | logger.Errorf(ctx, "error listening: %v", err) 103 | return 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cmds/server/support.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "context" 12 | ) 13 | 14 | // The code here supports instantiation of types within the main func. 15 | // We keep items here to avoid cluttering the main func. 16 | 17 | // shh is a example implementation of a simple secret provider that fulfills the private getSecret interface 18 | type shh struct{} 19 | 20 | // GetSecret ... 21 | func (s *shh) GetSecret(ctx context.Context, name, group string) ([]byte, error) { 22 | return []byte("cisco"), nil 23 | } 24 | -------------------------------------------------------------------------------- /cmds/server/tacquito.yaml: -------------------------------------------------------------------------------- 1 | # resuable references 2 | authenticator_type_bcrypt: &authenticator_type_bcrypt 1 3 | 4 | bcrypt: &bcrypt 5 | type: *authenticator_type_bcrypt 6 | options: 7 | # hashed value is `cisco` 8 | hash: 24326124313024596c6e6c3152305547304d55646f384c713254707165564836724f4664494a4f4266463864716362304632557a39635363582f4436 9 | 10 | privlvl_root: &privlvl_root 15 11 | action_deny: &action_deny 1 12 | action_permit: &action_permit 2 13 | 14 | # services 15 | exec: &exec 16 | name: exec 17 | set_values: 18 | - name: priv-lvl 19 | values: [ *privlvl_root ] 20 | 21 | shell: &shell 22 | name: shell 23 | match: 24 | # a rule that only matches if this service is applied to the localhost scope 25 | - name: scope 26 | values: [ localhost ] 27 | set_values: 28 | - name: magic 29 | values: [ vendor-strings ] 30 | 31 | # commands 32 | configure: &configure 33 | name: configure 34 | # commands are all regex based 35 | match: [.*] 36 | action: *action_permit 37 | 38 | show: &show 39 | name: show 40 | match: [.*] 41 | action: *action_permit 42 | 43 | bash: &bash 44 | name: bash 45 | match: 46 | - ls.* 47 | - pwd.* 48 | action: *action_permit 49 | 50 | pipe: &pipe 51 | name: pipe 52 | match: 53 | - grep.* 54 | - tail.* 55 | action: *action_permit 56 | 57 | # reusable references useful for groups 58 | # 59 | 60 | # accounter type which maps to tacquito/cmds/server/config/accounters/local 61 | accounter_type_file: &accounter_type_file 3 62 | 63 | # local file accounter 64 | file_accounter: &file_accounter 65 | # name is simply for the reader 66 | name: example_accounter 67 | # accounter type - this must be injected in main.go 68 | type: *accounter_type_file 69 | 70 | # groups 71 | rw: &rw 72 | # name must be globally unique 73 | name: read_write 74 | # service references 75 | services: 76 | - *exec 77 | - *shell 78 | # command references 79 | commands: 80 | - *bash 81 | - *configure 82 | - *pipe 83 | - *show 84 | # authenticator backend - this must be injected in main.go 85 | authenticator: *bcrypt 86 | # accounter backend - this must be injected in main.go 87 | accounter: *file_accounter 88 | 89 | 90 | # finally, declare users 91 | users: 92 | # name must be unique per scope 93 | - name: cisco 94 | # scopes to apply user to 95 | scopes: ["localhost"] 96 | # groups to apply on user 97 | groups: [*rw] 98 | # nb, no user level overrides exist so all user settings 99 | # get derived from the groups applied above 100 | 101 | # reusable references 102 | # 103 | 104 | # handler type used in SecretConfig 105 | handler_type_start: &handler_type_start 1 106 | 107 | # provider type used in SecretConfig 108 | provider_type_prefix: &provider_type_prefix 1 109 | 110 | # SecretProviders 111 | secrets: 112 | # SecretConfig 113 | - name: localhost 114 | # Keychain 115 | secret: 116 | group: tacquito 117 | # ideally this is not stored here in the clear but a safe secret backend is used to store/fetch from 118 | key: fooman 119 | # Handler - this must be injected in main.go 120 | handler: 121 | type: *handler_type_start 122 | # SecretProviderType - this must be injected in main.go 123 | type: *provider_type_prefix 124 | # Options are specific to the provider type and are map[str,str] 125 | # see provider implementation for details 126 | # tacquito/cmds/server/config/ 127 | options: 128 | prefixes: | 129 | [ 130 | "::0/0" 131 | ] 132 | 133 | prefix_allow: ["::0/0", "10.10.10.10/32"] 134 | prefix_deny: ["192.168.1.1/32"] 135 | -------------------------------------------------------------------------------- /cmds/server/test/acct_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package test 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "net" 14 | "os" 15 | "testing" 16 | 17 | tq "github.com/facebookincubator/tacquito" 18 | "github.com/facebookincubator/tacquito/cmds/server/log" 19 | 20 | "github.com/davecgh/go-spew/spew" 21 | "github.com/stretchr/testify/assert" 22 | ) 23 | 24 | func acctFlagStart(t *testing.T) []Test { 25 | var f tq.AcctRequestFlag 26 | f.Set(tq.AcctFlagStart) 27 | return []Test{ 28 | { 29 | Name: "accounting flag start", 30 | Secret: []byte("fooman"), 31 | Seq: []Sequence{ 32 | { 33 | Packet: tq.NewPacket( 34 | tq.SetPacketHeader( 35 | tq.NewHeader( 36 | tq.SetHeaderVersion(tq.Version{MajorVersion: tq.MajorVersion, MinorVersion: tq.MinorVersionDefault}), 37 | tq.SetHeaderType(tq.Accounting), 38 | tq.SetHeaderSessionID(1), 39 | ), 40 | ), 41 | tq.SetPacketBodyUnsafe( 42 | tq.NewAcctRequest( 43 | tq.SetAcctRequestFlag(f), 44 | tq.SetAcctRequestMethod(tq.AuthenMethodTacacsPlus), 45 | tq.SetAcctRequestPrivLvl(tq.PrivLvlRoot), 46 | tq.SetAcctRequestType(tq.AuthenTypeASCII), 47 | tq.SetAcctRequestService(tq.AuthenServiceLogin), 48 | tq.SetAcctRequestUser("mr_uses_group"), 49 | tq.SetAcctRequestArgs(tq.Args{"cmd=show", "cmd-arg=system"}), 50 | ), 51 | ), 52 | ), 53 | ValidateBody: func(response []byte) error { 54 | var body tq.AcctReply 55 | if err := tq.Unmarshal(response, &body); err != nil { 56 | return err 57 | } 58 | if body.Status != tq.AcctReplyStatusSuccess { 59 | spew.Dump(body) 60 | return fmt.Errorf("failed to match AcctReplyStatusSuccess") 61 | } 62 | return nil 63 | }, 64 | }, 65 | }, 66 | }, 67 | } 68 | 69 | } 70 | 71 | func TestAccounting(t *testing.T) { 72 | logger := log.New(30, os.Stderr) 73 | ctx := context.Background() 74 | sp, err := MockSecretProvider(ctx, logger, "testdata/test_config.yaml") 75 | assert.NoError(t, err) 76 | 77 | listener, err := net.Listen("tcp6", "[::1]:0") 78 | assert.NoError(t, err) 79 | tcpListener := listener.(*net.TCPListener) 80 | 81 | s := tq.NewServer(logger, sp) 82 | ctx, cancel := context.WithCancel(context.Background()) 83 | defer cancel() 84 | go func() { 85 | if err := s.Serve(ctx, tcpListener); err != nil { 86 | assert.NoError(t, err) 87 | } 88 | }() 89 | 90 | // append tests 91 | tests := []Test{} 92 | tests = append(tests, acctFlagStart(t)...) 93 | 94 | for _, test := range tests { 95 | c, err := tq.NewClient(tq.SetClientDialer("tcp6", listener.Addr().String(), test.Secret)) 96 | assert.NoError(t, err) 97 | assert.NotNil(t, c, "client was nil, bad") 98 | for _, s := range test.Seq { 99 | resp, err := c.Send(s.Packet) 100 | assert.NoError(t, err, "test name [%v]", test.Name) 101 | assert.NotNil(t, resp, "response was nil?") 102 | err = s.ValidateBody(resp.Body) 103 | assert.NoError(t, err, "test name [%v]", test.Name) 104 | } 105 | c.Close() 106 | } 107 | } 108 | 109 | func TestAccountingPacketNoAuthen(t *testing.T) { 110 | var f tq.AcctRequestFlag 111 | f.Set(tq.AcctFlagStart) 112 | ac := tq.NewAcctRequest( 113 | tq.SetAcctRequestFlag(f), 114 | tq.SetAcctRequestMethod(tq.AuthenMethodTacacsPlus), 115 | tq.SetAcctRequestPrivLvl(tq.PrivLvlRoot), 116 | tq.SetAcctRequestType(tq.AuthenTypeNotSet), 117 | tq.SetAcctRequestService(tq.AuthenServiceLogin), 118 | tq.SetAcctRequestUser("mr_uses_group"), 119 | tq.SetAcctRequestArgs(tq.Args{"cmd=show", "cmd-arg=system"}), 120 | ) 121 | if err := ac.Validate(); err != nil { 122 | t.Fatalf("unexpected error %v", err) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /cmds/server/test/authen_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package test 9 | 10 | import ( 11 | "context" 12 | "net" 13 | "os" 14 | "testing" 15 | 16 | tq "github.com/facebookincubator/tacquito" 17 | "github.com/facebookincubator/tacquito/cmds/server/log" 18 | 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | func TestAuthenticate(t *testing.T) { 23 | logger := log.New(30, os.Stderr) 24 | ctx := context.Background() 25 | sp, err := MockSecretProvider(ctx, logger, "testdata/test_config.yaml") 26 | assert.NoError(t, err) 27 | 28 | listener, err := net.Listen("tcp6", "[::1]:0") 29 | assert.NoError(t, err) 30 | tcpListener := listener.(*net.TCPListener) 31 | 32 | s := tq.NewServer(logger, sp) 33 | ctx, cancel := context.WithCancel(context.Background()) 34 | defer cancel() 35 | go func() { 36 | if err := s.Serve(ctx, tcpListener); err != nil { 37 | assert.NoError(t, err) 38 | } 39 | }() 40 | 41 | tests := []Test{ 42 | ASCIILoginFullFlow(), 43 | ASCIILoginEnable(), 44 | PapLoginFlow(), 45 | } 46 | 47 | tests = append(tests, GetASCIIEnableAbortTests()...) 48 | tests = append(tests, GetASCIILoginAbortTests()...) 49 | for _, test := range tests { 50 | c, err := tq.NewClient(tq.SetClientDialer("tcp6", listener.Addr().String(), test.Secret)) 51 | assert.NoError(t, err) 52 | for _, s := range test.Seq { 53 | resp, err := c.Send(s.Packet) 54 | assert.NoError(t, err, "test name [%v]", test.Name) 55 | err = s.ValidateBody(resp.Body) 56 | assert.NoError(t, err, "test name [%v]", test.Name) 57 | } 58 | c.Close() 59 | } 60 | } 61 | 62 | func TestAuthenticateNoAuthen(t *testing.T) { 63 | as := tq.NewAuthenStart( 64 | tq.SetAuthenStartAction(tq.AuthenActionLogin), 65 | tq.SetAuthenStartPrivLvl(tq.PrivLvlUser), 66 | tq.SetAuthenStartType(tq.AuthenTypeNotSet), 67 | tq.SetAuthenStartService(tq.AuthenServiceLogin), 68 | tq.SetAuthenStartPort("tty0"), 69 | tq.SetAuthenStartRemAddr("foo"), 70 | ) 71 | if err := as.Validate(); err == nil { 72 | t.Fatalf("expected error %v", err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cmds/server/test/author_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package test 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "net" 14 | "os" 15 | "testing" 16 | 17 | tq "github.com/facebookincubator/tacquito" 18 | "github.com/facebookincubator/tacquito/cmds/server/log" 19 | 20 | "github.com/davecgh/go-spew/spew" 21 | "github.com/stretchr/testify/assert" 22 | ) 23 | 24 | func basicAuthorPacket(username tq.AuthenUser, args tq.Args) *tq.Packet { 25 | return tq.NewPacket( 26 | tq.SetPacketHeader( 27 | tq.NewHeader( 28 | tq.SetHeaderVersion(tq.Version{MajorVersion: tq.MajorVersion, MinorVersion: tq.MinorVersionDefault}), 29 | tq.SetHeaderType(tq.Authorize), 30 | tq.SetHeaderRandomSessionID(), 31 | ), 32 | ), 33 | tq.SetPacketBodyUnsafe( 34 | tq.NewAuthorRequest( 35 | tq.SetAuthorRequestMethod(tq.AuthenMethodTacacsPlus), 36 | tq.SetAuthorRequestPrivLvl(tq.PrivLvlRoot), 37 | tq.SetAuthorRequestType(tq.AuthenTypeASCII), 38 | tq.SetAuthorRequestService(tq.AuthenServiceLogin), 39 | tq.SetAuthorRequestUser(username), 40 | tq.SetAuthorRequestArgs(args), 41 | ), 42 | ), 43 | ) 44 | } 45 | 46 | func authorCmdBased(t *testing.T) []Test { 47 | return []Test{ 48 | { 49 | Name: "authorization cmd flow 1", 50 | Secret: []byte("fooman"), 51 | Seq: []Sequence{ 52 | { 53 | Packet: basicAuthorPacket("mr_uses_group", tq.Args{"service=shell", "cmd=configure\n", "cmd-arg=terminal\n", "cmd-arg="}), 54 | ValidateBody: func(response []byte) error { 55 | var body tq.AuthorReply 56 | if err := tq.Unmarshal(response, &body); err != nil { 57 | return err 58 | } 59 | if body.Status != tq.AuthorStatusPassAdd { 60 | spew.Dump(body) 61 | return fmt.Errorf("failed to match AuthorStatusPassAdd") 62 | } 63 | return nil 64 | }, 65 | }, 66 | }, 67 | }, 68 | { 69 | Name: "authorization cmd flow 2 (line endings)", 70 | Secret: []byte("fooman"), 71 | Seq: []Sequence{ 72 | { 73 | Packet: basicAuthorPacket("mr_uses_group", tq.Args{"service=shell", "cmd=configure", "cmd-arg=terminal", "cmd-arg="}), 74 | ValidateBody: func(response []byte) error { 75 | var body tq.AuthorReply 76 | if err := tq.Unmarshal(response, &body); err != nil { 77 | return err 78 | } 79 | if body.Status != tq.AuthorStatusPassAdd { 80 | spew.Dump(body) 81 | return fmt.Errorf("failed to match AuthorStatusPassAdd") 82 | } 83 | return nil 84 | }, 85 | }, 86 | }, 87 | }, 88 | } 89 | } 90 | 91 | func authorSessionBased(t *testing.T) []Test { 92 | return []Test{ 93 | { 94 | Name: "authorization session flow 1", 95 | Secret: []byte("fooman"), 96 | Seq: []Sequence{ 97 | { 98 | Packet: basicAuthorPacket("mr_uses_group", tq.Args{"service=shell", "cmd=", "cisco-av-pair*", "shell:roles*"}), 99 | ValidateBody: func(response []byte) error { 100 | var body tq.AuthorReply 101 | if err := tq.Unmarshal(response, &body); err != nil { 102 | return err 103 | } 104 | if body.Status != tq.AuthorStatusPassRepl { 105 | spew.Dump(body) 106 | return fmt.Errorf("failed to match AuthorStatusPassRepl") 107 | } 108 | expectedArgs := tq.Args{"shell:roles*admin", "shell:roles*network-admin vdc-admin", "priv-lvl*15"} 109 | if !assert.Equal(t, expectedArgs, body.Args) { 110 | spew.Dump(body) 111 | return fmt.Errorf("failed to match Args") 112 | } 113 | return nil 114 | }, 115 | }, 116 | }, 117 | }, 118 | { 119 | Name: "authorization session flow 2 cmd* naked", 120 | Secret: []byte("fooman"), 121 | Seq: []Sequence{ 122 | { 123 | Packet: basicAuthorPacket("mr_uses_group", tq.Args{"service=shell", "cmd*"}), 124 | ValidateBody: func(response []byte) error { 125 | var body tq.AuthorReply 126 | if err := tq.Unmarshal(response, &body); err != nil { 127 | return err 128 | } 129 | if body.Status != tq.AuthorStatusPassRepl { 130 | spew.Dump(body) 131 | return fmt.Errorf("failed to match AuthorStatusPassRepl") 132 | } 133 | expectedArgs := tq.Args{"priv-lvl*15"} 134 | if !assert.Equal(t, expectedArgs, body.Args) { 135 | spew.Dump(body) 136 | return fmt.Errorf("failed to match Args") 137 | } 138 | return nil 139 | }, 140 | }, 141 | }, 142 | }, 143 | } 144 | } 145 | func TestAuthorize(t *testing.T) { 146 | logger := log.New(30, os.Stderr) 147 | ctx := context.Background() 148 | sp, err := MockSecretProvider(ctx, logger, "testdata/test_config.yaml") 149 | assert.NoError(t, err) 150 | 151 | listener, err := net.Listen("tcp6", "[::1]:0") 152 | assert.NoError(t, err) 153 | tcpListener := listener.(*net.TCPListener) 154 | 155 | s := tq.NewServer(logger, sp) 156 | ctx, cancel := context.WithCancel(context.Background()) 157 | defer cancel() 158 | go func() { 159 | if err := s.Serve(ctx, tcpListener); err != nil { 160 | assert.NoError(t, err) 161 | } 162 | }() 163 | 164 | // append tests 165 | tests := []Test{} 166 | tests = append(tests, authorCmdBased(t)...) 167 | tests = append(tests, authorSessionBased(t)...) 168 | 169 | for _, test := range tests { 170 | c, err := tq.NewClient(tq.SetClientDialer("tcp6", listener.Addr().String(), test.Secret)) 171 | assert.NoError(t, err) 172 | for _, s := range test.Seq { 173 | resp, err := c.Send(s.Packet) 174 | assert.NoError(t, err, "test name [%v]", test.Name) 175 | err = s.ValidateBody(resp.Body) 176 | assert.NoError(t, err, "test name [%v]", test.Name) 177 | } 178 | c.Close() 179 | } 180 | } 181 | 182 | func TestAuthorizePacketNoAuthen(t *testing.T) { 183 | username := tq.AuthenUser("testuser") 184 | args := tq.Args{"service=shell", "cmd=configure\n", "cmd-arg=terminal\n", "cmd-arg="} 185 | az := tq.NewAuthorRequest( 186 | tq.SetAuthorRequestMethod(tq.AuthenMethodTacacsPlus), 187 | tq.SetAuthorRequestPrivLvl(tq.PrivLvlRoot), 188 | tq.SetAuthorRequestType(tq.AuthenTypeNotSet), 189 | tq.SetAuthorRequestService(tq.AuthenServiceLogin), 190 | tq.SetAuthorRequestUser(username), 191 | tq.SetAuthorRequestArgs(args), 192 | ) 193 | 194 | if err := az.Validate(); err != nil { 195 | t.Fatalf("unexpected error %v", err) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /cmds/server/test/config_yaml.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package test 9 | 10 | import "os" 11 | 12 | // SampleConfig ... 13 | const SampleConfig = ` 14 | # resuable references 15 | authenticator_type_bcrypt: &authenticator_type_bcrypt 1 16 | 17 | privlvl_root: &privlvl_root 15 18 | 19 | action_deny: &action_deny 1 20 | action_permit: &action_permit 2 21 | 22 | logger_type_stderr: &logger_type_stderr 1 23 | logger_type_syslog: &logger_type_syslog 2 24 | logger_type_file: &logger_type_file 3 25 | 26 | logger_stderr: &logger_stderr 27 | name: stderr 28 | type: *logger_type_stderr 29 | 30 | logger_syslog: &logger_syslog 31 | name: syslog 32 | type: *logger_type_syslog 33 | 34 | logger_file: &logger_file 35 | name: file 36 | type: *logger_type_file 37 | 38 | bcrypt: &bcrypt 39 | type: *authenticator_type_bcrypt 40 | options: 41 | # password 42 | hash: 24326124313024614d6761663134486e35366b6a734b2f79564a384b2e577678754c6b34314364586a4d727a6276794a7844304c4371757345765171 43 | 44 | # services 45 | cmd: &cmd 46 | name: cmd 47 | is_optional: true 48 | set_values: 49 | - name: priv-lvl 50 | values: [15] 51 | is_optional: true 52 | 53 | cisco_avp: &cisco_avp 54 | name: cisco-av-pair 55 | is_optional: true 56 | set_values: 57 | - name: shell:roles 58 | values: [ admin ] 59 | is_optional: true 60 | - name: shell:roles 61 | values: [ network-admin vdc-admin ] 62 | is_optional: true 63 | 64 | # commands 65 | conf_t: &conf_t 66 | name: configure 67 | match: [terminal, exclusive] 68 | action: *action_permit 69 | 70 | conf_b: &conf_b 71 | name: configure 72 | match: [batch] 73 | action: *action_permit 74 | 75 | # groups 76 | noc: &noc 77 | name: noc 78 | services: [*cisco_avp, *cmd] 79 | commands: [*conf_t, *conf_b] 80 | authenticator: *bcrypt 81 | accounter: *logger_file 82 | 83 | 84 | # finally, declare users 85 | users: 86 | - name: mr_uses_group 87 | scopes: ["localhost"] 88 | groups: [*noc] 89 | - name: mr_no_group 90 | scopes: ["localhost"] 91 | services: [*cisco_avp] 92 | commands: [*conf_t] 93 | authenticator: *bcrypt 94 | accounter: *logger_file 95 | - name: ms_commands_only 96 | scopes: ["localhost"] 97 | commands: [*conf_t] 98 | 99 | 100 | handler_type_start: &handler_type_start 1 101 | handler_type_span: &handler_type_span 2 102 | 103 | provider_type_prefix: &provider_type_prefix 1 104 | provider_type_dns: &provider_type_dns 2 105 | 106 | secrets: 107 | - name: localhost 108 | secret: 109 | group: tacquito 110 | key: fooman 111 | handler: 112 | type: *handler_type_start 113 | type: *provider_type_prefix 114 | options: 115 | prefixes: | 116 | [ 117 | "::0/0" 118 | ] 119 | ` 120 | 121 | // WriteSampleConfig write config to current root 122 | func WriteSampleConfig(path string) error { 123 | return os.WriteFile(path, []byte(SampleConfig), 0644) 124 | } 125 | -------------------------------------------------------------------------------- /cmds/server/test/mocks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package test 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | 14 | tq "github.com/facebookincubator/tacquito" 15 | "github.com/facebookincubator/tacquito/cmds/server/config" 16 | 17 | "github.com/facebookincubator/tacquito/cmds/server/config/accounters/local" 18 | "github.com/facebookincubator/tacquito/cmds/server/config/authenticators/bcrypt" 19 | "github.com/facebookincubator/tacquito/cmds/server/config/authorizers/stringy" 20 | "github.com/facebookincubator/tacquito/cmds/server/config/secret" 21 | "github.com/facebookincubator/tacquito/cmds/server/config/secret/prefix" 22 | "github.com/facebookincubator/tacquito/cmds/server/handlers" 23 | "github.com/facebookincubator/tacquito/cmds/server/loader" 24 | "github.com/facebookincubator/tacquito/cmds/server/loader/yaml" 25 | ) 26 | 27 | // loggerProvider provides the logging implementation 28 | type loggerProvider interface { 29 | Infof(ctx context.Context, format string, args ...interface{}) 30 | Errorf(ctx context.Context, format string, args ...interface{}) 31 | Debugf(ctx context.Context, format string, args ...interface{}) 32 | // Record provides a structed log interface for systems that need a record based format 33 | Record(ctx context.Context, r map[string]string, obscure ...string) 34 | Set(ctx context.Context, fields map[string]string, keys ...tq.ContextKey) context.Context 35 | } 36 | 37 | // Test ... 38 | type Test struct { 39 | Name string 40 | Secret []byte 41 | Seq []Sequence 42 | } 43 | 44 | // Sequence ... 45 | type Sequence struct { 46 | Packet *tq.Packet 47 | ValidateHeader func(header *tq.Header) error 48 | ValidateBody func(response []byte) error 49 | Validate func(p *tq.Packet) error 50 | } 51 | 52 | // MockSecretProvider creates a mock secret provider 53 | func MockSecretProvider(ctx context.Context, logger loggerProvider, configPath string) (tq.SecretProvider, error) { 54 | accountingLogger, err := local.New(logger, local.SetLogSinkDefault("/tmp/tacquito_accounting.log", "tacquito")) 55 | if err != nil { 56 | return nil, fmt.Errorf("error building accounting logger; %v", err) 57 | } 58 | sp, err := loader.NewLocalConfig( 59 | ctx, 60 | configPath, 61 | yaml.New(), 62 | loader.SetLoggerProvider(logger), 63 | loader.SetKeychainProvider(secret.New()), 64 | loader.SetConfigProvider(config.New()), 65 | loader.SetAuthorizerProvider(stringy.New(logger)), 66 | loader.RegisterSecretProviderType(config.PREFIX, prefix.New(logger)), 67 | loader.RegisterAuthenticator(config.BCRYPT, bcrypt.New(logger, &shh{})), 68 | loader.RegisterAccounter(config.FILE, accountingLogger), 69 | loader.RegisterHandlerType(config.START, handlers.NewStart(logger)), 70 | ) 71 | if err != nil { 72 | return nil, err 73 | } 74 | sp.BlockUntilLoaded() 75 | return sp, nil 76 | } 77 | 78 | type shh struct{} 79 | 80 | // GetSecret ... 81 | func (s *shh) GetSecret(ctx context.Context, name, group string) ([]byte, error) { 82 | fmt.Println(name, group) 83 | return []byte("cisco"), nil 84 | } 85 | -------------------------------------------------------------------------------- /cmds/server/test/server_bench_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package test 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "io" 14 | "net" 15 | "testing" 16 | 17 | tq "github.com/facebookincubator/tacquito" 18 | "github.com/facebookincubator/tacquito/cmds/server/log" 19 | 20 | "github.com/davecgh/go-spew/spew" 21 | "github.com/stretchr/testify/assert" 22 | ) 23 | 24 | // BenchmarkPacketExchangeAsciiLoginSingleClient will test the full ascii login flow 25 | // using a single client instance 26 | func BenchmarkPacketExchangeAsciiLoginUsingSharedClient(b *testing.B) { 27 | logger := log.New(0, io.Discard) // no logs 28 | ctx := context.Background() 29 | sp, err := MockSecretProvider(ctx, logger, "testdata/test_config.yaml") 30 | assert.NoError(b, err) 31 | 32 | listener, err := net.Listen("tcp6", "[::1]:0") 33 | assert.NoError(b, err) 34 | tcpListener := listener.(*net.TCPListener) 35 | 36 | s := tq.NewServer(logger, sp) 37 | ctx, cancel := context.WithCancel(context.Background()) 38 | defer cancel() 39 | go func() { 40 | if err := s.Serve(ctx, tcpListener); err != nil { 41 | assert.NoError(b, err) 42 | } 43 | }() 44 | 45 | c, err := tq.NewClient(tq.SetClientDialer("tcp6", listener.Addr().String(), []byte("fooman"))) 46 | assert.NoError(b, err) 47 | defer c.Close() 48 | 49 | test := ASCIILoginFullFlow() 50 | // record allocations regardless of go test -test.bench 51 | b.ReportAllocs() 52 | for n := 0; n < b.N; n++ { 53 | for _, s := range test.Seq { 54 | c.Send(s.Packet) 55 | } 56 | } 57 | } 58 | 59 | // benchTest is used for allocation testing 60 | type benchTest struct { 61 | name string 62 | fn func(b *testing.B) 63 | expected func(name string, r testing.BenchmarkResult) 64 | } 65 | 66 | // TestPacketExchangeAsciiLoginUsingSharedClientAllocation provides data on the allocs/op we do 67 | // for a given request 68 | func TestPacketExchangeAsciiLoginUsingSharedClientAllocation(t *testing.T) { 69 | tests := []benchTest{ 70 | { 71 | name: "BenchmarkPacketExchangeAsciiLoginUsingSharedClient", 72 | fn: BenchmarkPacketExchangeAsciiLoginUsingSharedClient, 73 | expected: func(name string, r testing.BenchmarkResult) { 74 | t.Log(spew.Sdump(r)) 75 | expectedAllocs := 25 76 | actual := r.AllocsPerOp() 77 | assert.EqualValues(t, expectedAllocs, actual, fmt.Sprintf("%s allocations were not nominal; wanted %v got %v", name, expectedAllocs, actual)) 78 | }, 79 | }, 80 | } 81 | for _, test := range tests { 82 | r := testing.Benchmark(test.fn) 83 | test.expected(test.name, r) 84 | } 85 | } 86 | 87 | // BenchmarkPacketExchangeAsciiLoginSingleClient will test the full ascii login flow 88 | // using a new client instance each loop 89 | func BenchmarkPacketExchangeAsciiLoginUsingNewClient(b *testing.B) { 90 | logger := log.New(0, io.Discard) // no logs 91 | ctx := context.Background() 92 | sp, err := MockSecretProvider(ctx, logger, "testdata/test_config.yaml") 93 | assert.NoError(b, err) 94 | 95 | listener, err := net.Listen("tcp6", "[::1]:0") 96 | assert.NoError(b, err) 97 | tcpListener := listener.(*net.TCPListener) 98 | 99 | s := tq.NewServer(logger, sp) 100 | ctx, cancel := context.WithCancel(context.Background()) 101 | defer cancel() 102 | go func() { 103 | if err := s.Serve(ctx, tcpListener); err != nil { 104 | assert.NoError(b, err) 105 | } 106 | }() 107 | 108 | test := ASCIILoginFullFlow() 109 | for n := 0; n < b.N; n++ { 110 | c, err := tq.NewClient(tq.SetClientDialer("tcp6", listener.Addr().String(), []byte("fooman"))) 111 | assert.NoError(b, err) 112 | for _, s := range test.Seq { 113 | c.Send(s.Packet) 114 | } 115 | c.Close() 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /cmds/server/test/testdata/test_config.yaml: -------------------------------------------------------------------------------- 1 | # resuable references 2 | authenticator_type_bcrypt: &authenticator_type_bcrypt 1 3 | 4 | privlvl_root: &privlvl_root 15 5 | 6 | action_deny: &action_deny 1 7 | action_permit: &action_permit 2 8 | 9 | logger_type_stderr: &logger_type_stderr 1 10 | logger_type_syslog: &logger_type_syslog 2 11 | logger_type_file: &logger_type_file 3 12 | 13 | logger_stderr: &logger_stderr 14 | name: stderr 15 | type: *logger_type_stderr 16 | 17 | logger_syslog: &logger_syslog 18 | name: syslog 19 | type: *logger_type_syslog 20 | 21 | logger_file: &logger_file 22 | name: file 23 | type: *logger_type_file 24 | 25 | bcrypt: &bcrypt 26 | type: *authenticator_type_bcrypt 27 | options: 28 | # password 29 | hash: 24326124313024614d6761663134486e35366b6a734b2f79564a384b2e577678754c6b34314364586a4d727a6276794a7844304c4371757345765171 30 | 31 | # services 32 | cmd: &cmd 33 | name: cmd 34 | is_optional: true 35 | set_values: 36 | - name: priv-lvl 37 | values: [15] 38 | is_optional: true 39 | 40 | cisco_avp: &cisco_avp 41 | name: cisco-av-pair 42 | is_optional: true 43 | set_values: 44 | - name: shell:roles 45 | values: [ admin ] 46 | is_optional: true 47 | - name: shell:roles 48 | values: [ network-admin vdc-admin ] 49 | is_optional: true 50 | 51 | # commands 52 | conf_t: &conf_t 53 | name: configure 54 | match: [terminal, exclusive] 55 | action: *action_permit 56 | 57 | conf_b: &conf_b 58 | name: configure 59 | match: [batch] 60 | action: *action_permit 61 | 62 | # groups 63 | noc: &noc 64 | name: noc 65 | services: [*cisco_avp, *cmd] 66 | commands: [*conf_t, *conf_b] 67 | authenticator: *bcrypt 68 | accounter: *logger_file 69 | 70 | 71 | # finally, declare users 72 | users: 73 | - name: mr_uses_group 74 | scopes: ["localhost"] 75 | groups: [*noc] 76 | - name: mr_no_group 77 | scopes: ["localhost"] 78 | services: [*cisco_avp] 79 | commands: [*conf_t] 80 | authenticator: *bcrypt 81 | accounter: *logger_file 82 | - name: ms_commands_only 83 | scopes: ["localhost"] 84 | commands: [*conf_t] 85 | 86 | 87 | handler_type_start: &handler_type_start 1 88 | handler_type_span: &handler_type_span 2 89 | 90 | provider_type_prefix: &provider_type_prefix 1 91 | provider_type_dns: &provider_type_dns 2 92 | 93 | secrets: 94 | - name: localhost 95 | secret: 96 | group: tacquito 97 | key: fooman 98 | handler: 99 | type: *handler_type_start 100 | type: *provider_type_prefix 101 | options: 102 | prefixes: | 103 | [ 104 | "::0/0" 105 | ] 106 | -------------------------------------------------------------------------------- /crypt_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/davecgh/go-spew/spew" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | // returns an encrypted TACACs+ packet's byte values, contains the 12 byte header 19 | // encrypted with secret []byte("fooman") 20 | func getEncryptedBytes() []byte { 21 | return []byte{0xc1, 0x01, 0x01, 0x00, 0x00, 0x00, 0x30, 0x39, 0x00, 0x00, 0x00, 0x2c, 0x9c, 0xed, 0x73, 22 | 0xaa, 0x3d, 0x6d, 0x2f, 0x1f, 0xef, 0x62, 0x98, 0x73, 0xf0, 0xac, 0x2f, 0x11, 0x8a, 0xe2, 0x89, 0x8a, 23 | 0xcb, 0x50, 0x72, 0xb2, 0x6d, 0xd2, 0xec, 0xab, 0xe1, 0x4e, 0x22, 0x64, 0x4c, 0x7c, 0xb2, 0xe, 0x43, 24 | 0xe, 0x33, 0x92, 0x85, 0x47, 0xca, 0xfc} 25 | } 26 | 27 | // returns a decrypted TACACs+ packet's byte values, does NOT contain the 12 byte header 28 | func getDecryptedBytes() []byte { 29 | return []byte{0x01, 0x01, 0x01, 0x01, 0x05, 0x0B, 0x14, 0x00, 0x61, 0x64, 0x6D, 0x69, 0x6E, 0x63, 0x6F, 30 | 0x6D, 0x6D, 0x61, 0x6E, 0x64, 0x2D, 0x61, 0x70, 0x69, 0x32, 0x30, 0x30, 0x31, 0x3A, 0x34, 0x38, 31 | 0x36, 0x30, 0x3A, 0x34, 0x38, 0x36, 0x30, 0x3A, 0x3A, 0x38, 0x38, 0x38, 0x38} 32 | } 33 | 34 | func TestDecrypt(t *testing.T) { 35 | encrypted := getEncryptedBytes() 36 | decrypted := getDecryptedBytes() 37 | var header Header 38 | err := Unmarshal(encrypted[:12], &header) 39 | assert.NoError(t, err) 40 | packet := &Packet{Header: &header, Body: encrypted[12:]} 41 | 42 | err = crypt([]byte("fooman"), packet) 43 | assert.NoError(t, err) 44 | 45 | assert.Equal(t, decrypted, packet.Body) 46 | var body AuthenStart 47 | err = Unmarshal(packet.Body, &body) 48 | assert.NoError(t, err) 49 | t.Log(spew.Sdump(body)) 50 | } 51 | 52 | func TestEncrypt(t *testing.T) { 53 | encrypted := getEncryptedBytes() 54 | decrypted := getDecryptedBytes() 55 | 56 | body, _ := NewAuthenStart( 57 | SetAuthenStartAction(AuthenActionLogin), 58 | SetAuthenStartPrivLvl(PrivLvlUser), 59 | SetAuthenStartType(AuthenTypeASCII), 60 | SetAuthenStartService(AuthenServiceLogin), 61 | SetAuthenStartUser("admin"), 62 | SetAuthenStartPort("command-api"), 63 | SetAuthenStartRemAddr("2001:4860:4860::8888"), 64 | ).MarshalBinary() 65 | 66 | packet := NewPacket( 67 | SetPacketHeader( 68 | NewHeader( 69 | SetHeaderVersion(Version{MajorVersion: MajorVersion, MinorVersion: MinorVersionOne}), 70 | SetHeaderType(Authenticate), 71 | SetHeaderSessionID(12345), 72 | ), 73 | ), 74 | SetPacketBody(body), 75 | ) 76 | t.Log(spew.Sdump(packet)) 77 | err := crypt([]byte("fooman"), packet) 78 | assert.NoError(t, err) 79 | t.Log(spew.Sdump(packet)) 80 | assert.Equal(t, encrypted[12:], packet.Body) 81 | err = crypt([]byte("fooman"), packet) 82 | assert.NoError(t, err) 83 | t.Log(spew.Sdump(packet)) 84 | assert.Equal(t, decrypted, packet.Body) 85 | } 86 | 87 | func TestEncryptDecryptSecretMismatch(t *testing.T) { 88 | body := NewAuthenReply( 89 | SetAuthenReplyStatus(AuthenStatusGetUser), 90 | SetAuthenReplyServerMsg("\nUser Access Verification\n\nUsername:"), 91 | ) 92 | b, _ := body.MarshalBinary() 93 | 94 | packet := NewPacket( 95 | SetPacketHeader( 96 | NewHeader( 97 | SetHeaderVersion(Version{MajorVersion: MajorVersion, MinorVersion: MinorVersionOne}), 98 | SetHeaderType(Authenticate), 99 | SetHeaderSessionID(12345), 100 | ), 101 | ), 102 | SetPacketBody(b), 103 | ) 104 | 105 | secret := []byte("chilled cow") 106 | err := crypt(secret, packet) 107 | assert.NoError(t, err) 108 | 109 | // We need to ensure Encrypt and Decrypt operations result in an error if a secret mismatches 110 | // ensure secret mismatch causes an error 111 | secret = []byte("imma bad secret") 112 | err = crypt(secret, packet) 113 | assert.NoError(t, err) 114 | 115 | // Unmarshal decrypted bytes back into original packet body type 116 | // this should cause a malformed packet error because of a secret mismatch when encrypt/decrypt 117 | newAuthenReply := &AuthenReply{} 118 | err = newAuthenReply.UnmarshalBinary(packet.Body) 119 | assert.Error(t, err, "a bad secret change should have caused this packet to be malformed") 120 | assert.NotEqual(t, *body, *newAuthenReply) 121 | } 122 | 123 | func TestPacketEncryptDecryptUnencryptFlagSet(t *testing.T) { 124 | body := NewAuthenReply( 125 | SetAuthenReplyStatus(AuthenStatusGetUser), 126 | SetAuthenReplyServerMsg("\nUser Access Verification\n\nUsername:"), 127 | ) 128 | b, _ := body.MarshalBinary() 129 | 130 | packet := NewPacket( 131 | SetPacketHeader( 132 | NewHeader( 133 | SetHeaderVersion(Version{MajorVersion: MajorVersion, MinorVersion: MinorVersionOne}), 134 | SetHeaderType(Authenticate), 135 | SetHeaderFlag(UnencryptedFlag), 136 | SetHeaderSessionID(12345), 137 | ), 138 | ), 139 | SetPacketBody(b), 140 | ) 141 | 142 | secret := []byte("chilled cow") 143 | 144 | err := crypt(secret, packet) 145 | assert.NoError(t, err) 146 | assert.Equal(t, b, packet.Body) 147 | 148 | err = crypt(secret, packet) 149 | assert.NoError(t, err) 150 | assert.Equal(t, b, packet.Body) 151 | } 152 | 153 | // benchTest is used for allocation testing 154 | type benchTest struct { 155 | name string 156 | fn func(b *testing.B) 157 | expected func(name string, r testing.BenchmarkResult) 158 | } 159 | 160 | func TestCrypterAllocation(t *testing.T) { 161 | tests := []benchTest{ 162 | { 163 | name: "encrypt", 164 | fn: BenchmarkCrypterAllocation, 165 | expected: func(name string, r testing.BenchmarkResult) { 166 | t.Log(spew.Sdump(r)) 167 | expectedAllocs := 4 168 | actual := r.AllocsPerOp() 169 | assert.EqualValues(t, expectedAllocs, actual, fmt.Sprintf("%s allocations were not nominal; wanted %v got %v", name, expectedAllocs, actual)) 170 | }, 171 | }, 172 | } 173 | for _, test := range tests { 174 | r := testing.Benchmark(test.fn) 175 | test.expected(test.name, r) 176 | } 177 | } 178 | 179 | // BenchmarkCrypterAllocation benchmarks the allocs/op crypter takes when called with crypted 180 | // or decrypted bytes. Since the op is the same in both directions we only test one form of it 181 | func BenchmarkCrypterAllocation(b *testing.B) { 182 | encrypted := getEncryptedBytes() 183 | var header Header 184 | Unmarshal(encrypted[:12], &header) 185 | packet := &Packet{Header: &header, Body: encrypted[12:]} 186 | secret := []byte("fooman") 187 | 188 | // record allocations regardless of go test -test.bench 189 | b.ReportAllocs() 190 | for range b.N { 191 | crypt(secret, packet) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /ctx_keys.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | 7 | Use this file to store context keys 8 | */ 9 | 10 | package tacquito 11 | 12 | // ContextKey is used in Request contexts 13 | type ContextKey string 14 | 15 | // ContextReqID ... 16 | const ContextReqID ContextKey = "reqID" 17 | 18 | // ContextSessionID is used to store the context for a session in Request as a wrapped context 19 | const ContextSessionID ContextKey = "session-id" 20 | 21 | // ContextConnRemoteAddr is used to store the net.conn remoteAddr within a session. This value would be present 22 | // in any sub contexts that share the underlying net.conn 23 | const ContextConnRemoteAddr ContextKey = "conn-remote-addr" 24 | 25 | // ContextConnLocalAddr is the tacquito server address 26 | const ContextConnLocalAddr ContextKey = "conn-local-addr" 27 | 28 | // ContextUser is used to store the username within a session. 29 | const ContextUser ContextKey = "user" 30 | 31 | // ContextUserMsg ... 32 | const ContextUserMsg ContextKey = "user-msg" 33 | 34 | // ContextRemoteAddr ... 35 | const ContextRemoteAddr ContextKey = "rem-addr" 36 | 37 | // ContextReqArgs for logging context arguments with replies 38 | const ContextReqArgs ContextKey = "req-args" 39 | 40 | // ContextAcctType ... 41 | const ContextAcctType ContextKey = "type" 42 | 43 | // ContextFlags logs the flags attribute of Accounting requests 44 | const ContextFlags ContextKey = "flags" 45 | 46 | // ContextPrivLvl ... 47 | const ContextPrivLvl ContextKey = "priv-lvl" 48 | 49 | // ContextPort ... 50 | const ContextPort ContextKey = "port" 51 | 52 | /* durations 53 | these ctx keys are being stored for request specific tracking of 54 | expensive operations. We already have prometheus Summary metrics tracking 55 | some of timings of these operatings, but they don't expose the level of detail we need 56 | for performance tracking and client debugging 57 | */ 58 | 59 | // ContextLoaderDuration is total processing time taken by loader i.e how long 60 | // it takes for the loader to map an IP to a scope 61 | const ContextLoaderDuration ContextKey = "loader_duration_ms" 62 | -------------------------------------------------------------------------------- /docs/imgs/service_overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/tacquito/01f62577b24a7754831f9409dead3dcd67b35fec/docs/imgs/service_overview.jpg -------------------------------------------------------------------------------- /docs/imgs/tacquito-mascot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/tacquito/01f62577b24a7754831f9409dead3dcd67b35fec/docs/imgs/tacquito-mascot.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/facebookincubator/tacquito 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 7 | github.com/fsnotify/fsnotify v1.5.4 8 | github.com/google/uuid v1.3.0 9 | github.com/prometheus/client_golang v1.13.0 10 | github.com/stretchr/testify v1.8.0 11 | golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 12 | gopkg.in/yaml.v3 v3.0.1 13 | ) 14 | 15 | require ( 16 | github.com/beorn7/perks v1.0.1 // indirect 17 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 18 | github.com/golang/protobuf v1.5.2 // indirect 19 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | github.com/prometheus/client_model v0.2.0 // indirect 22 | github.com/prometheus/common v0.37.0 // indirect 23 | github.com/prometheus/procfs v0.8.0 // indirect 24 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 25 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 26 | google.golang.org/protobuf v1.28.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "context" 12 | ) 13 | 14 | // Writer is an abstraction used for adding Writers to the response object 15 | type Writer interface { 16 | Write(ctx context.Context, p []byte) (int, error) 17 | } 18 | 19 | // response implements the Response interface. when testing handlers, provide your own 20 | // mock of this struct via the interface. crypt operations are not exposed for testing. 21 | type response struct { 22 | loggerProvider 23 | ctx context.Context 24 | crypter *crypter 25 | next Handler 26 | // header is the corresponding header that was used to create this response 27 | header Header 28 | // slice of writers to write back the response 29 | writers []Writer 30 | } 31 | 32 | // Reply will write the provided EncoderDecoder to the underlying net.Conn. This method handles 33 | // all header values based on the underlying EncoderDecoder. If you want total control on the 34 | // packet that is written, use Send instead. 35 | func (r *response) Reply(v EncoderDecoder) (int, error) { 36 | seqNo := int(r.header.SeqNo) 37 | // some special conditions for different body types 38 | switch t := v.(type) { 39 | case *AuthenReply: 40 | if t.Status == AuthenStatusRestart { 41 | seqNo = 1 42 | } else { 43 | seqNo++ 44 | } 45 | default: 46 | seqNo++ 47 | } 48 | header := NewHeader( 49 | SetHeaderVersion(r.header.Version), 50 | SetHeaderType(r.header.Type), 51 | SetHeaderSeqNo(seqNo), 52 | SetHeaderFlag(r.header.Flags), 53 | SetHeaderSessionID(r.header.SessionID), 54 | ) 55 | b, err := v.MarshalBinary() 56 | if err != nil { 57 | r.Errorf(r.ctx, "unable to marshal packet; %v", err) 58 | return 0, err 59 | } 60 | r.header = *header 61 | p := NewPacket( 62 | SetPacketHeader(header), 63 | SetPacketBody(b), 64 | ) 65 | if pbytes, err := p.MarshalBinary(); err == nil { 66 | for _, mw := range r.writers { 67 | _, err := mw.Write(r.ctx, pbytes) 68 | if err != nil { 69 | r.Errorf(r.ctx, "unable to write to response writer; %v", err) 70 | } 71 | } 72 | } 73 | return r.Write(p) 74 | } 75 | 76 | // Write will write the packet to the underlying net.Conn. If you are expecting another packet 77 | // to return from the client after writing a response, call Next(handler) to provide a next Handler. 78 | func (r *response) Write(p *Packet) (int, error) { 79 | return r.crypter.write(p) 80 | } 81 | 82 | // Next sets the incoming handler to next. This is only used for exchange sequences within the authenticate 83 | // packet types 84 | func (r *response) Next(next Handler) { 85 | r.next = next 86 | } 87 | 88 | func (r *response) RegisterWriter(mw Writer) { 89 | r.writers = append(r.writers, mw) 90 | } 91 | 92 | func (r *response) Context(ctx context.Context) { 93 | r.ctx = ctx 94 | } 95 | 96 | // ReplyWithContext can be used to reply to requests that cause a server error or failure in processing of response. 97 | // This method includes an additional variadic argument `writers` that can be used to write the response `v` to 98 | // other sinks (eg logging backends) 99 | // This method also overwrites the response's context with the supplied `ctx` 100 | func (r *response) ReplyWithContext(ctx context.Context, v EncoderDecoder, writers ...Writer) (int, error) { 101 | r.Context(ctx) 102 | for _, w := range writers { 103 | if w != nil { 104 | r.RegisterWriter(w) 105 | } 106 | } 107 | return r.Reply(v) 108 | } 109 | 110 | // Response controls what we send back to the client. Calls to Write should be considered final on the 111 | // packet back to the client. You may not call Exchange after Write. 112 | type Response interface { 113 | Reply(v EncoderDecoder) (int, error) 114 | ReplyWithContext(ctx context.Context, v EncoderDecoder, writers ...Writer) (int, error) 115 | Write(p *Packet) (int, error) 116 | Next(next Handler) 117 | RegisterWriter(Writer) 118 | // Context sets context of response to ctx 119 | Context(ctx context.Context) 120 | } 121 | 122 | // Request provides access to the config for this net.Conn and also the packet itself 123 | type Request struct { 124 | Header Header 125 | Body []byte 126 | Context context.Context 127 | } 128 | 129 | // Fields will extract all fields from any packet type and attempt to include any optional 130 | // ContextKey values 131 | func (r Request) Fields(keys ...ContextKey) map[string]string { 132 | allFields := r.Header.Fields() 133 | 134 | // add optional context values 135 | if r.Context != nil { 136 | for _, key := range keys { 137 | v, ok := r.Context.Value(key).(string) 138 | if ok { 139 | allFields[string(key)] = v 140 | } 141 | } 142 | } 143 | 144 | // merge will add our header fields to the body 145 | // the rfc doesn't contain fields that collide 146 | merge := func(a, b map[string]string) { 147 | for k, v := range b { 148 | a[k] = v 149 | } 150 | } 151 | switch r.Header.Type { 152 | case Authenticate: 153 | var as AuthenStart 154 | if err := Unmarshal(r.Body, &as); err == nil { 155 | merge(allFields, as.Fields()) 156 | return allFields 157 | } 158 | var ac AuthenContinue 159 | if err := Unmarshal(r.Body, &ac); err == nil { 160 | merge(allFields, ac.Fields()) 161 | return allFields 162 | } 163 | var ar AuthenReply 164 | if err := Unmarshal(r.Body, &ar); err == nil { 165 | merge(allFields, ar.Fields()) 166 | return allFields 167 | } 168 | 169 | case Authorize: 170 | var ar AuthorRequest 171 | if err := Unmarshal(r.Body, &ar); err == nil { 172 | merge(allFields, ar.Fields()) 173 | return allFields 174 | } 175 | var arr AuthorReply 176 | if err := Unmarshal(r.Body, &arr); err == nil { 177 | merge(allFields, arr.Fields()) 178 | return allFields 179 | } 180 | 181 | case Accounting: 182 | var ar AcctRequest 183 | if err := Unmarshal(r.Body, &ar); err == nil { 184 | merge(allFields, ar.Fields()) 185 | return allFields 186 | } 187 | var arr AcctReply 188 | if err := Unmarshal(r.Body, &arr); err == nil { 189 | merge(allFields, arr.Fields()) 190 | return allFields 191 | } 192 | } 193 | // unknown packet 194 | return nil 195 | } 196 | 197 | // Handler form the basis for the state machine during client server exchanges. 198 | type Handler interface { 199 | Handle(response Response, request Request) 200 | } 201 | 202 | // HandlerFunc is an adapter that allows higher order functions to be used as Handler interfaces 203 | type HandlerFunc func(response Response, request Request) 204 | 205 | // Handle satisfies the Handler interface 206 | func (h HandlerFunc) Handle(response Response, request Request) { 207 | h(response, request) 208 | } 209 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "encoding/binary" 12 | "fmt" 13 | rand "math/rand/v2" 14 | ) 15 | 16 | // 17 | // tacplus header 18 | // https://datatracker.ietf.org/doc/html/rfc8907#section-4.1 19 | // 20 | 21 | // HeaderOption used to modify existing headers that were decoded and reuse 22 | // them in a response, or to create a new Header with options 23 | type HeaderOption func(*Header) 24 | 25 | // SetHeaderVersion sets Version 26 | func SetHeaderVersion(v Version) HeaderOption { 27 | return func(h *Header) { 28 | h.Version = v 29 | } 30 | } 31 | 32 | // SetHeaderType sets packet type 33 | func SetHeaderType(v HeaderType) HeaderOption { 34 | return func(h *Header) { 35 | h.Type = v 36 | } 37 | } 38 | 39 | // SetHeaderSeqNo sets SequenceNumber to a specific value 40 | func SetHeaderSeqNo(v int) HeaderOption { 41 | return func(h *Header) { 42 | h.SeqNo = SequenceNumber(v) 43 | } 44 | } 45 | 46 | // SetHeaderFlag sets HeaderFlag to a specific value 47 | // This field contains various bitmapped flags. 48 | func SetHeaderFlag(v HeaderFlag) HeaderOption { 49 | return func(h *Header) { 50 | h.Flags = v 51 | } 52 | } 53 | 54 | // SetHeaderSessionID sets SessionID to a specific value. 55 | // This number MUST be generated by a 56 | // cryptographically strong random number generation method. 57 | func SetHeaderSessionID(v SessionID) HeaderOption { 58 | return func(h *Header) { 59 | h.SessionID = v 60 | } 61 | } 62 | 63 | // SetHeaderLen sets the length of the header. This is automatically done for you 64 | // but if you wish to set a length explictly for tests... 65 | func SetHeaderLen(v int) HeaderOption { 66 | return func(h *Header) { 67 | h.Length = uint32(v) 68 | } 69 | } 70 | 71 | // SetHeaderRandomSessionID sets a weaker math/rand session id. To meet the requirements 72 | // of the rfc, you should use SetHeaderSessionID with a cryptographically strong random number. 73 | // this setter should only be used in examples and tests 74 | func SetHeaderRandomSessionID() HeaderOption { 75 | return func(h *Header) { 76 | h.SessionID = SessionID(rand.Uint32()) 77 | } 78 | } 79 | 80 | // MaxHeaderLength defines a fixed size for a tacacs header 81 | const MaxHeaderLength = 0x0c 82 | 83 | // NewHeader will create a new Header based on the provided options, starting with 84 | // common defaults. the defaults will be overwritten, if provided in the options 85 | func NewHeader(opts ...HeaderOption) *Header { 86 | h := &Header{} 87 | var f HeaderFlag 88 | defaults := []HeaderOption{ 89 | SetHeaderSeqNo(1), 90 | SetHeaderFlag(f), 91 | } 92 | opts = append(defaults, opts...) 93 | for _, opt := range opts { 94 | opt(h) 95 | } 96 | return h 97 | } 98 | 99 | // Header holds the tacacs header fields found in all tacacs packet types. 100 | type Header struct { 101 | Version Version 102 | Type HeaderType 103 | SeqNo SequenceNumber 104 | SessionID SessionID 105 | Flags HeaderFlag 106 | Length uint32 107 | } 108 | 109 | // Validate all fields on this type 110 | func (h *Header) Validate() error { 111 | // validate 112 | for _, t := range []Field{h.Version, h.Type, h.SeqNo} { 113 | if err := t.Validate(nil); err != nil { 114 | return err 115 | } 116 | } 117 | // manually validate Length since it's not a Field interface 118 | if h.Length > MaxBodyLength { 119 | return fmt.Errorf("length field is too large, max size is 2^(16)") 120 | } 121 | return nil 122 | } 123 | 124 | // MarshalBinary encodes Header into tacacs bytes 125 | func (h *Header) MarshalBinary() ([]byte, error) { 126 | // validate 127 | if err := h.Validate(); err != nil { 128 | return nil, err 129 | } 130 | buf := make([]byte, MaxHeaderLength) 131 | version, err := h.Version.MarshalBinary() 132 | if err != nil { 133 | return nil, err 134 | } 135 | buf[0] = version[0] 136 | buf[1] = uint8(h.Type) 137 | buf[2] = uint8(h.SeqNo) 138 | buf[3] = uint8(h.Flags) 139 | binary.BigEndian.PutUint32(buf[4:], uint32(h.SessionID)) 140 | binary.BigEndian.PutUint32(buf[8:], h.Length) 141 | return buf, nil 142 | } 143 | 144 | // UnmarshalBinary decodes tacacs bytes into Header 145 | func (h *Header) UnmarshalBinary(data []byte) error { 146 | if len(data) < MaxHeaderLength { 147 | return fmt.Errorf("Header size [%v] is not matched to expected size [%v]", len(data), MaxHeaderLength) 148 | } 149 | var version Version 150 | err := version.UnmarshalBinary(data) 151 | if err != nil { 152 | return err 153 | } 154 | h.Version = version 155 | h.Type = HeaderType(data[1]) 156 | h.SeqNo = SequenceNumber(data[2]) 157 | h.Flags = HeaderFlag(data[3]) 158 | h.SessionID = SessionID(binary.BigEndian.Uint32(data[4:])) 159 | h.Length = binary.BigEndian.Uint32(data[8:]) 160 | 161 | // set SingleConnect. Ignored on all other sequence numbers 162 | // see https://datatracker.ietf.org/doc/html/rfc8907#section-4.3 163 | // if this is our first response, we always set this. its up to the 164 | // client to persist it beyond from seq 2 165 | if h.SeqNo == 2 { 166 | h.Flags.Set(SingleConnect) 167 | } 168 | 169 | // validate 170 | if err := h.Validate(); err != nil { 171 | return err 172 | } 173 | return nil 174 | } 175 | 176 | // Fields returns fields from this packet compatible with a structured logger 177 | func (h Header) Fields() map[string]string { 178 | // ensure fields don't collide with packet body values 179 | // prefix with header- 180 | return map[string]string{ 181 | "header-version": h.Version.String(), 182 | "header-type": h.Type.String(), 183 | "header-seq-no": h.SeqNo.String(), 184 | "header-session-id": h.SessionID.String(), 185 | "header-flags": h.Flags.String(), 186 | "header-length": fmt.Sprint(h.Length), 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /header_fields_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | package tacquito 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestSequenceNumber(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | validate func() 19 | }{ 20 | { 21 | name: "below lower bound", 22 | validate: func() { 23 | err := SequenceNumber(0).Validate(nil) 24 | assert.Error(t, err, "sequence 0 should be invalid") 25 | }, 26 | }, 27 | { 28 | name: "lower bound", 29 | validate: func() { 30 | err := SequenceNumber(1).Validate(nil) 31 | assert.NoError(t, err, "sequence 1 should be valid") 32 | }, 33 | }, 34 | { 35 | name: "upper bound", 36 | validate: func() { 37 | err := SequenceNumber(HeaderMaxSequence).Validate(nil) 38 | assert.NoError(t, err, "sequence value of 2 ^ 8 - 1 should be valid") 39 | }, 40 | }, 41 | { 42 | name: "beyond upper bound", 43 | validate: func() { 44 | err := SequenceNumber(HeaderMaxSequence + 1).Validate(nil) 45 | assert.Error(t, err, "sequence number beyond 2 ^ 8 - 1 should be invalid") 46 | }, 47 | }, 48 | { 49 | name: "invalid client sequence", 50 | validate: func() { 51 | err := ClientSequenceNumber(2).Validate(nil) 52 | assert.Error(t, err, "even sequence number should be invalid") 53 | }, 54 | }, 55 | { 56 | name: "invalid exchange sequence", 57 | validate: func() { 58 | last := SequenceNumber(4) 59 | current := SequenceNumber(3) 60 | err := LastSequence(last).Validate(current) 61 | assert.Error(t, err, "sequence numbers must be monotonic") 62 | }, 63 | }, 64 | } 65 | 66 | for _, test := range tests { 67 | test.validate() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import "context" 11 | 12 | // loggerProvider provides the logging implementation 13 | type loggerProvider interface { 14 | Infof(ctx context.Context, format string, args ...interface{}) 15 | Errorf(ctx context.Context, format string, args ...interface{}) 16 | Debugf(ctx context.Context, format string, args ...interface{}) 17 | // Record provides a structed log interface for systems that need a record based format 18 | Record(ctx context.Context, r map[string]string, obscure ...string) 19 | } 20 | -------------------------------------------------------------------------------- /proxy/readerwriter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Package proxy provides a reader writer that can add PROXY ASCII strings to bytes 9 | // or strip the PROXY ASCII strings from bytes. The context is appropriately 10 | // updated against the underlying so as to preserve the remote host's ability to "see" the client 11 | // address and port. 12 | // Only the ASCII portion is implemented from http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt 13 | package proxy 14 | 15 | import ( 16 | "bytes" 17 | "fmt" 18 | "io" 19 | "net" 20 | "strings" 21 | ) 22 | 23 | // MaxProxyHeader is the max size needed to scan for and obtain a proxy header string 24 | const ( 25 | MaxProxyHeader = 108 26 | ) 27 | 28 | // HeaderStringMalformed is returned when we found a PROXY header string but it was malformed 29 | // for some reason 30 | type HeaderStringMalformed string 31 | 32 | func (e HeaderStringMalformed) Error() string { return string(e) } 33 | 34 | // NewHeader returns a ReaderWriter that implements the HA PROXY ASCII encode/decode 35 | func NewHeader(client, remote net.Addr) *Header { 36 | return &Header{client: client, remote: remote} 37 | } 38 | 39 | // Header will operate on []byte to add or remove the ASCII proxy header. This type 40 | // can be composed into another to satisfy a net.Conn if desired. Be sure not to override 41 | // LocalAddr and RemoteAddr in doing so and take care to sequence the Read/Write calls. 42 | type Header struct { 43 | client net.Addr 44 | remote net.Addr 45 | } 46 | 47 | func (h *Header) Read(b []byte) (int, error) { 48 | header := h.proxyHeader() 49 | if len(b) < len(header) { 50 | return 0, io.ErrShortBuffer 51 | } 52 | return copy(b, header), nil 53 | } 54 | 55 | func (h *Header) proxyHeader() []byte { 56 | // spec requires uppercase for network 57 | network := strings.ToUpper(h.client.Network()) 58 | clientIP, clientPort := getIPPort(h.client) 59 | if clientIP == nil { 60 | return nil 61 | } 62 | proxyIP, proxyPort := getIPPort(h.remote) 63 | if proxyIP == nil { 64 | return nil 65 | } 66 | // PROXY \r\n 67 | return []byte( 68 | fmt.Sprintf( 69 | "PROXY %s %s %s %d %d\r\n\x00", 70 | network, 71 | clientIP, 72 | proxyIP, 73 | clientPort, 74 | proxyPort, 75 | ), 76 | ) 77 | } 78 | 79 | // Write will take a well formed proxy header and write it to self. 80 | // b will be stripped if line endings such as \r\n prior to calling since 81 | // scanning for these is a function of a higher layer such as bufio.Reader.ReadLine() 82 | func (h *Header) Write(b []byte) (int, error) { 83 | if !bytes.Contains(b, []byte(`PROXY`)) { 84 | return 0, HeaderStringMalformed("no proxy prefix detected on header") 85 | } 86 | // ensure we don't have a \r\n\x00 (null byte at end) 87 | b = bytes.TrimSuffix(b, []byte("\r\n\x00")) 88 | // scan for: 89 | // PROXY \r\n 90 | // spec: http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt 91 | // 92 | 93 | // the entire text we are interested must be contained within 108 bytes 94 | // and we limit it as such. Only a properly formed 95 | // proxy string will pass through this process successfully. 96 | 97 | // we really only should be able to scan one line 98 | line := string(b) 99 | chunks := strings.Split(line, " ") 100 | if len(chunks) != 6 { 101 | // first case we do not read the underlying due to undefined data 102 | return len(b), HeaderStringMalformed(fmt.Sprintf("proxy line [%v] is malformed. expected len==6, got [%v]; after split; %v", line, len(chunks), chunks)) 103 | } 104 | switch network := strings.ToLower(chunks[1]); network { 105 | case "tcp", "tcp6", "tcp4": 106 | h.client = &addr{network: network, address: chunks[2], port: chunks[4]} 107 | h.remote = &addr{network: network, address: chunks[3], port: chunks[5]} 108 | return len(b), nil 109 | } 110 | // this is the second case we don't read the underlying due to undefined data 111 | return len(b), net.UnknownNetworkError(fmt.Sprintf("%v", chunks[1])) 112 | } 113 | 114 | // LocalAddr ... 115 | func (h *Header) LocalAddr() net.Addr { return h.client } 116 | 117 | // RemoteAddr ... 118 | func (h *Header) RemoteAddr() net.Addr { return h.remote } 119 | 120 | func getIPPort(a net.Addr) (net.IP, int) { 121 | t, ok := a.(*net.TCPAddr) 122 | if !ok { 123 | return nil, 0 124 | } 125 | return t.IP, t.Port 126 | } 127 | 128 | // addr is a shortcut implementation to providing a net.Addr 129 | type addr struct { 130 | network string 131 | address string 132 | port string 133 | } 134 | 135 | func (a addr) Network() string { return a.network } 136 | func (a addr) String() string { 137 | return net.JoinHostPort(a.address, a.port) 138 | } 139 | 140 | // NoProxyHeader is returned when the proxy writer code is called 141 | // but there was no proxy header found. It should be non-terminal 142 | // and only used to continue processing a message as if no header was 143 | // there. Any additional failures will be handled accordingly downstream 144 | type NoProxyHeader string 145 | 146 | func (n NoProxyHeader) Error() string { 147 | return string(n) 148 | } 149 | -------------------------------------------------------------------------------- /proxy/readerwriter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package proxy 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "net" 14 | "testing" 15 | 16 | "github.com/davecgh/go-spew/spew" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func TestPeekHAProxy(t *testing.T) { 21 | tests := []struct { 22 | line []byte 23 | clientAddress string 24 | clientNetwork string 25 | remoteAddress string 26 | remoteNetwork string 27 | errorExpected func(t *testing.T, err error) 28 | }{ 29 | { 30 | line: []byte("PROXY TCP6 2401:db00:eef0:1120:3520:0000:1802:1 2401:db00:eef0:1120:3520:0000:1802:61ee 100 200\r\n\x00"), 31 | clientAddress: "[2401:db00:eef0:1120:3520:0000:1802:1]:100", 32 | clientNetwork: "tcp6", 33 | remoteAddress: "[2401:db00:eef0:1120:3520:0000:1802:61ee]:200", 34 | remoteNetwork: "tcp6", 35 | }, 36 | { 37 | line: []byte("PROXY TCP 2401:db00:eef0:1120:3520:0000:1802:2 2401:db00:eef0:1120:3520:0000:1802:61ee 100 200\r\n\x00"), 38 | clientAddress: "[2401:db00:eef0:1120:3520:0000:1802:2]:100", 39 | clientNetwork: "tcp", 40 | remoteAddress: "[2401:db00:eef0:1120:3520:0000:1802:61ee]:200", 41 | remoteNetwork: "tcp", 42 | }, 43 | { 44 | line: []byte("PROXY TCP5 2401:db00:eef0:1120:3520:0000:1802:3 2401:db00:eef0:1120:3520:0000:1802:61ee 100 200\r\n\x00"), 45 | clientAddress: ":", 46 | remoteAddress: ":", 47 | errorExpected: func(t *testing.T, err error) { 48 | var expectedErr net.UnknownNetworkError 49 | if errors.As(err, &expectedErr) { 50 | return 51 | } 52 | assert.Fail(t, fmt.Sprintf("expected a net.UnknownNetworkError, got %v", err)) 53 | }, 54 | }, 55 | { 56 | line: []byte("PROXY TCP4 1.1.1.1 2.2.2.2 100 200\r\n\x00"), 57 | clientAddress: "1.1.1.1:100", 58 | clientNetwork: "tcp4", 59 | remoteAddress: "2.2.2.2:200", 60 | remoteNetwork: "tcp4", 61 | }, 62 | { 63 | line: []byte("asdfjfkldj;lalsdkjflkjdsl;ajl;sdjfioew;aoijsldjfaol;wieja;olsdjfoai;wejafl"), 64 | clientAddress: ":", 65 | remoteAddress: ":", 66 | errorExpected: func(t *testing.T, err error) { 67 | var expectedErr HeaderStringMalformed 68 | if errors.As(err, &expectedErr) { 69 | return 70 | } 71 | assert.Fail(t, fmt.Sprintf("expected a HeaderStringMalformed, got %v", err)) 72 | }, 73 | }, 74 | { 75 | line: []byte("PROXY"), 76 | clientAddress: ":", 77 | remoteAddress: ":", 78 | errorExpected: func(t *testing.T, err error) { 79 | var expectedErr HeaderStringMalformed 80 | if errors.As(err, &expectedErr) { 81 | return 82 | } 83 | assert.Fail(t, fmt.Sprintf("expected a HeaderStringMalformed, got %v", err)) 84 | }, 85 | }, 86 | } 87 | for _, test := range tests { 88 | pw := NewHeader(&addr{}, &addr{}) 89 | _, err := pw.Write(test.line) 90 | if test.errorExpected != nil { 91 | test.errorExpected(t, err) 92 | } else { 93 | assert.NoError(t, err) 94 | } 95 | 96 | spew.Dump(pw) 97 | assert.Equal(t, test.clientAddress, pw.client.String()) 98 | assert.Equal(t, test.clientNetwork, pw.client.Network()) 99 | assert.Equal(t, test.remoteAddress, pw.remote.String()) 100 | assert.Equal(t, test.remoteNetwork, pw.remote.Network()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /request_fields_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "context" 12 | "testing" 13 | ) 14 | 15 | func TestRequestFields(t *testing.T) { 16 | // we need a request body that will successfully unmarshal 17 | acctRequest := NewAcctRequest( 18 | SetAcctRequestMethod(AuthenMethodTacacsPlus), 19 | SetAcctRequestPrivLvl(PrivLvlRoot), 20 | SetAcctRequestType(AuthenTypeASCII), 21 | SetAcctRequestService(AuthenServiceLogin), 22 | SetAcctRequestPort("4"), 23 | SetAcctRequestRemAddr("async"), 24 | ) 25 | acctBody, err := acctRequest.MarshalBinary() 26 | if err != nil { 27 | t.Error("failed to marshal an AccountRequest, uh oh") 28 | } 29 | 30 | // helper to add multiple values to a context 31 | withValues := func(ctx context.Context, kv map[ContextKey]string) context.Context { 32 | for k, v := range kv { 33 | ctx = context.WithValue(ctx, k, v) 34 | } 35 | return ctx 36 | } 37 | 38 | tests := []struct { 39 | name string 40 | request Request 41 | expected map[string]string 42 | ctxKeys []ContextKey 43 | }{ 44 | { 45 | name: "ensure ContextKeys are added to fields map", 46 | request: Request{Header: *NewHeader(SetHeaderType(Accounting)), Body: acctBody, Context: withValues(context.Background(), map[ContextKey]string{ContextSessionID: "123", ContextReqID: "1", ContextConnRemoteAddr: "9.9.9.9"})}, 47 | expected: map[string]string{string(ContextSessionID): "123", string(ContextReqID): "1", string(ContextConnRemoteAddr): "9.9.9.9"}, 48 | ctxKeys: []ContextKey{ContextSessionID, ContextReqID, ContextConnRemoteAddr}, 49 | }, 50 | } 51 | 52 | for _, test := range tests { 53 | fields := test.request.Fields(test.ctxKeys...) 54 | for expectedKey, expectedValue := range test.expected { 55 | if v, ok := fields[expectedKey]; !ok || v != expectedValue { 56 | t.Fatalf("request fields dont match, got %v, wanted %v", fields, test.expected) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /secret.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "context" 12 | "net" 13 | ) 14 | 15 | // SecretProvider is responsible for secret selection for incoming client connections 16 | // It provides configuration items for the server to process any connections that originate 17 | // on a given net.Conn. Only the RemoteAddr is provided to make this determination. 18 | type SecretProvider interface { 19 | Get(ctx context.Context, remote net.Addr) ([]byte, Handler, error) 20 | } 21 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "context" 12 | "errors" 13 | "io" 14 | "net" 15 | "time" 16 | 17 | "github.com/prometheus/client_golang/prometheus" 18 | ) 19 | 20 | // Option is used to set optional behaviors on the server. Required behaviors are set 21 | // in NewServer. Omitting options will not adversely affect the service 22 | type Option func(s *Server) 23 | 24 | // SetUseProxy will enable ASCII based proxy support defined by 25 | // http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt 26 | func SetUseProxy(v bool) Option { 27 | return func(s *Server) { 28 | s.proxy = v 29 | } 30 | } 31 | 32 | // NewServer returns a new server. 33 | // loggerProvider - the logging backend to use 34 | // listener - net.Listener 35 | // sp SecretProvider - enables server to translate net.conn.remaddr into associated config for that device 36 | func NewServer(l loggerProvider, sp SecretProvider, opts ...Option) *Server { 37 | s := &Server{loggerProvider: l, SecretProvider: sp} 38 | for _, opt := range opts { 39 | opt(s) 40 | } 41 | return s 42 | } 43 | 44 | // Server ... 45 | type Server struct { 46 | loggerProvider 47 | waitGroup 48 | SecretProvider 49 | 50 | // enables ha-proxy ascii proxy header support 51 | proxy bool 52 | } 53 | 54 | // DeadlineListener is a net.Listener that supports Deadlines 55 | type DeadlineListener interface { 56 | net.Listener 57 | SetDeadline(t time.Time) error 58 | } 59 | 60 | // Serve is a blocking method that serves clients 61 | func (s *Server) Serve(ctx context.Context, listener DeadlineListener) error { 62 | defer func() { 63 | s.Infof(ctx, "Stopping server listener for %v...", listener.Addr().String()) 64 | err := listener.Close() 65 | if err != nil { 66 | s.Errorf(ctx, "%s", err) 67 | } 68 | s.Infof(ctx, "waiting for [%v] connections to close prior to shutdown", s.active) 69 | s.Wait() 70 | }() 71 | 72 | for { 73 | select { 74 | case <-ctx.Done(): 75 | return nil 76 | default: 77 | serveReceived.Inc() 78 | // the 10 second deadline implies there is a limit to how long downstream handlers 79 | // may take to respond to a client. Clients may also give up much sooner than this 80 | // deadline. Be mindful of this when adjusting. 81 | if err := listener.SetDeadline(time.Now().Add(10 * time.Second)); err != nil { 82 | s.Errorf(ctx, "cannot set listener deadline; %s", err) 83 | } 84 | conn, err := listener.Accept() 85 | if err != nil { 86 | var opE *net.OpError 87 | if errors.As(err, &opE) { 88 | if !opE.Temporary() { 89 | serveAcceptedError.Inc() 90 | return nil 91 | } 92 | if opE.Temporary() { 93 | // triggered by SetDeadline 94 | continue 95 | } 96 | // something else? fall through 97 | } 98 | s.Errorf(ctx, "server error in serving request: %s", err) 99 | serveAcceptedError.Inc() 100 | continue 101 | } 102 | s.Add(1) 103 | go s.serve(ctx, conn) 104 | } 105 | } 106 | } 107 | 108 | func (s *Server) serve(ctx context.Context, conn net.Conn) { 109 | defer s.Done() 110 | timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { 111 | ms := v * 1000 // make milliseconds 112 | connectionDuration.Observe(ms) 113 | })) 114 | defer timer.ObserveDuration() 115 | // start a timer to measure loader duration 116 | loaderStart := time.Now() 117 | secret, handler, err := s.Get(ctx, conn.RemoteAddr()) 118 | if err != nil || secret == nil || handler == nil { 119 | s.Errorf(ctx, "ignoring request: %v", err) 120 | conn.Close() 121 | timer.ObserveDuration() 122 | return 123 | } 124 | ctx = context.WithValue(ctx, ContextLoaderDuration, time.Since(loaderStart).Milliseconds()) 125 | serveAccepted.Inc() 126 | s.handle(ctx, newCrypter(secret, conn, s.proxy), handler) 127 | serveAccepted.Dec() 128 | } 129 | 130 | // handle will process connections on a net.Conn. This is meant to be executed in a goroutine 131 | func (s *Server) handle(ctx context.Context, c *crypter, h Handler) { 132 | // defer closing the connection on return. 133 | defer c.Close() 134 | // scoped to the entire undelrying net.Conn. this is needed for single-connect 135 | sessionProvider := newSessionProvider() 136 | defer sessionProvider.close() 137 | for { 138 | select { 139 | case <-ctx.Done(): 140 | s.Debugf(ctx, "context cancellation received, closing connection to %v", c.RemoteAddr()) 141 | return 142 | default: 143 | if err := c.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil { 144 | s.Errorf(ctx, "unable to set read deadline on connection %v", c.RemoteAddr()) 145 | } 146 | packet, err := c.read() 147 | if err != nil { 148 | if err != io.EOF { 149 | s.Errorf(ctx, "closing connection, unable to read, %v", err) 150 | } 151 | return 152 | } 153 | // store basic connection parameters into ctx 154 | ctxWithAddr := context.WithValue(ctx, ContextConnRemoteAddr, strip(c.RemoteAddr().String())) 155 | ctxWithAddr = context.WithValue(ctxWithAddr, ContextConnLocalAddr, c.LocalAddr().String()) 156 | 157 | // create our request 158 | req := Request{ 159 | Header: *packet.Header, 160 | Body: packet.Body, 161 | Context: ctxWithAddr, 162 | } 163 | // create the response 164 | resp := &response{ctx: req.Context, crypter: c, loggerProvider: s.loggerProvider, header: req.Header} 165 | state, err := sessionProvider.get(req.Header) 166 | if err != nil { 167 | s.Errorf(ctx, "unable to obtain a session; connection will close; %v", err) 168 | return 169 | } 170 | // default to our provided handler for new flows 171 | if state == nil { 172 | state = h 173 | sessionProvider.set(req.Header, nil) 174 | } 175 | handlers.Inc() 176 | state.Handle(resp, req) 177 | handlers.Dec() 178 | if resp.next == nil { 179 | s.Debugf(ctx, "[%v] sessionID is complete", req.Header.SessionID) 180 | sessionProvider.delete(req.Header.SessionID) 181 | continue 182 | } 183 | sessionProvider.update(resp.header, resp.next) 184 | } 185 | } 186 | } 187 | 188 | // strip removes port and [] from an IP address 189 | // on a best effort basis. In case of any error, the 190 | // original input is returned 191 | func strip(ip string) string { 192 | host, _, err := net.SplitHostPort(ip) 193 | if err != nil { 194 | return ip 195 | } 196 | return host 197 | } 198 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | func TestStrip(t *testing.T) { 15 | type test struct { 16 | input string 17 | want string 18 | } 19 | 20 | tests := []test{ 21 | {input: "1.1.1.1", want: "1.1.1.1"}, 22 | {input: "1.1.1.1:23", want: "1.1.1.1"}, 23 | {input: "2001:db8:0:1:1:1:1:1", want: "2001:db8:0:1:1:1:1:1"}, 24 | {input: "2001:db8:0:1:1::1", want: "2001:db8:0:1:1::1"}, 25 | {input: "[2001:db8:0:1:1:1:1:1]:23", want: "2001:db8:0:1:1:1:1:1"}, 26 | } 27 | 28 | for _, tc := range tests { 29 | got := strip(tc.input) 30 | if got != tc.want { 31 | t.Fatalf("unexpected output from strip function: want: %v, got: %v", tc.want, got) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sessions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "fmt" 12 | "sync" 13 | 14 | "github.com/prometheus/client_golang/prometheus" 15 | ) 16 | 17 | // newSessionProvider creates a session manager for an underlying net.Conn 18 | func newSessionProvider() *sessions { 19 | return &sessions{known: make(map[SessionID]*sessionContext)} 20 | } 21 | 22 | // sessionContext is a thread safe cache that tracks session ids from clients 23 | type sessionContext struct { 24 | header Header 25 | Handler 26 | timer *prometheus.Timer 27 | } 28 | 29 | // sessions manages client session ids. we use sessions to know how to 30 | // handle older exchange methods that require multiple packet exchanges 31 | // in reality, this is really only significant for ascii login flows or for 32 | // long running accounting flows. Per the rfc, sessions are assumed valid 33 | // from the client. 34 | type sessions struct { 35 | sync.RWMutex 36 | known map[SessionID]*sessionContext 37 | } 38 | 39 | // get a session 40 | func (s *sessions) get(h Header) (Handler, error) { 41 | if err := ClientSequenceNumber(h.SeqNo).Validate(nil); err != nil { 42 | s.delete(h.SessionID) 43 | return nil, fmt.Errorf("sessionID [%v] sequence number is corrupted; %v", h.SessionID, err) 44 | } 45 | s.Lock() 46 | defer s.Unlock() 47 | sc, ok := s.known[h.SessionID] 48 | if !ok { 49 | sessionsGetMiss.Inc() 50 | return nil, nil 51 | } 52 | if err := LastSequence(sc.header.SeqNo).Validate(h.SeqNo); err != nil { 53 | return nil, fmt.Errorf("sessionID [%v] sequence number is mismatched; %v", h.SessionID, err) 54 | } 55 | sessionsGetHit.Inc() 56 | return sc.Handler, nil 57 | } 58 | 59 | // set a session and next handler. for long running packet exchanges, we need 60 | // to know what handler state was left when we last responded so we know what to 61 | // processes the next client response as. This is especially important when we 62 | // are using single-connect because we could have multiple packets from multiple 63 | // sessions being multiplexed on one connection. 64 | func (s *sessions) set(h Header, n Handler) { 65 | s.Lock() 66 | defer s.Unlock() 67 | sessionsActive.Inc() 68 | sessionsSet.Inc() 69 | timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { 70 | ms := v * 1000 // make milliseconds 71 | sessionDurations.Observe(ms) 72 | })) 73 | s.known[h.SessionID] = &sessionContext{header: h, Handler: n, timer: timer} 74 | } 75 | 76 | // update a session id and next handler. 77 | func (s *sessions) update(h Header, n Handler) { 78 | s.Lock() 79 | defer s.Unlock() 80 | sc, ok := s.known[h.SessionID] 81 | if !ok { 82 | sessionsGetMiss.Inc() 83 | return 84 | } 85 | sc.header = h 86 | sc.Handler = n 87 | s.known[h.SessionID] = sc 88 | } 89 | 90 | // delete a session 91 | func (s *sessions) delete(session SessionID) { 92 | s.Lock() 93 | defer s.Unlock() 94 | sessionsActive.Dec() 95 | if sc := s.known[session]; sc != nil { 96 | sc.timer.ObserveDuration() 97 | } 98 | delete(s.known, session) 99 | } 100 | 101 | // close will stop all prom timers, it's the only reason we have this 102 | func (s *sessions) close() { 103 | for _, r := range s.known { 104 | r.timer.ObserveDuration() 105 | } 106 | } 107 | 108 | // waitGroup wraps sync.WaitGroup and exposes 109 | // a counter that can be used in Serve() 110 | type waitGroup struct { 111 | sync.WaitGroup 112 | active uint 113 | } 114 | 115 | // Add adds to WaitGroup and increments the count 116 | func (w *waitGroup) Add(delta int) { 117 | waitgroupActive.Inc() 118 | w.WaitGroup.Add(delta) 119 | w.active++ 120 | } 121 | 122 | // Done decrements WaitGroup and the counter 123 | func (w *waitGroup) Done() { 124 | waitgroupActive.Dec() 125 | w.WaitGroup.Done() 126 | w.active-- 127 | } 128 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package tacquito 9 | 10 | import ( 11 | "github.com/prometheus/client_golang/prometheus" 12 | ) 13 | 14 | var ( 15 | // gauges and counters 16 | serveReceived = prometheus.NewCounter(prometheus.CounterOpts{ 17 | Namespace: "tacquito", 18 | Name: "serve_received", 19 | Help: "total number of packets received within the server", 20 | }) 21 | serveAccepted = prometheus.NewGauge(prometheus.GaugeOpts{ 22 | Namespace: "tacquito", 23 | Name: "serve_accepted", 24 | Help: "number of accepted connections within the server that are currently being processed", 25 | }) 26 | serveAcceptedError = prometheus.NewCounter(prometheus.CounterOpts{ 27 | Namespace: "tacquito", 28 | Name: "serve_accepted_error", 29 | Help: "number of accepted connection errors within the server", 30 | }) 31 | handlers = prometheus.NewGauge(prometheus.GaugeOpts{ 32 | Namespace: "tacquito", 33 | Name: "handle_handlers", 34 | Help: "number of handlers running within the server", 35 | }) 36 | crypterRead = prometheus.NewCounter(prometheus.CounterOpts{ 37 | Namespace: "tacquito", 38 | Name: "crypter_read", 39 | Help: "number of crypt reads within the server", 40 | }) 41 | crypterReadError = prometheus.NewCounter(prometheus.CounterOpts{ 42 | Namespace: "tacquito", 43 | Name: "crypter_read_error", 44 | Help: "number of crypt read errors within the server", 45 | }) 46 | crypterWrite = prometheus.NewCounter(prometheus.CounterOpts{ 47 | Namespace: "tacquito", 48 | Name: "crypter_write", 49 | Help: "number of crypt writes within the server", 50 | }) 51 | crypterWriteError = prometheus.NewCounter(prometheus.CounterOpts{ 52 | Namespace: "tacquito", 53 | Name: "crypter_write_error", 54 | Help: "number of crypt write errors within the server", 55 | }) 56 | crypterBadSecret = prometheus.NewCounter(prometheus.CounterOpts{ 57 | Namespace: "tacquito", 58 | Name: "crypter_badSecret", 59 | Help: "number of bad secrets", 60 | }) 61 | crypterUnmarshalError = prometheus.NewCounter(prometheus.CounterOpts{ 62 | Namespace: "tacquito", 63 | Name: "crypter_unmarshal_error", 64 | Help: "number of errors unmarshalling in crypter", 65 | }) 66 | crypterCryptError = prometheus.NewCounter(prometheus.CounterOpts{ 67 | Namespace: "tacquito", 68 | Name: "crypter_crypt_error", 69 | Help: "number of errors in crypter crypt()", 70 | }) 71 | crypterMarshalError = prometheus.NewCounter(prometheus.CounterOpts{ 72 | Namespace: "tacquito", 73 | Name: "crypter_marshal_error", 74 | Help: "number of errors marshalling in crypter", 75 | }) 76 | waitgroupActive = prometheus.NewGauge(prometheus.GaugeOpts{ 77 | Namespace: "tacquito", 78 | Name: "waitgroup_handle_routines_active", 79 | Help: "number of active waitgroup go routines within the server", 80 | }) 81 | sessionsActive = prometheus.NewGauge(prometheus.GaugeOpts{ 82 | Namespace: "tacquito", 83 | Name: "sessions_active", 84 | Help: "number of active sessions within the server", 85 | }) 86 | sessionsGetHit = prometheus.NewCounter(prometheus.CounterOpts{ 87 | Namespace: "tacquito", 88 | Name: "sessions_get_hit", 89 | Help: "number of session cache hits within the server", 90 | }) 91 | sessionsGetMiss = prometheus.NewCounter(prometheus.CounterOpts{ 92 | Namespace: "tacquito", 93 | Name: "sessions_get_miss", 94 | Help: "number of session cache misses within the server", 95 | }) 96 | sessionsSet = prometheus.NewCounter(prometheus.CounterOpts{ 97 | Namespace: "tacquito", 98 | Name: "sessions_set", 99 | Help: "number of session set in the cache", 100 | }) 101 | 102 | // durations 103 | sessionDurations = prometheus.NewSummary( 104 | prometheus.SummaryOpts{ 105 | Namespace: "tacquito", 106 | Name: "sessions_duration_milliseconds", 107 | Help: "the time a session is a live within tacquito, in milliseconds", 108 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, 109 | }, 110 | ) 111 | 112 | connectionDuration = prometheus.NewSummary( 113 | prometheus.SummaryOpts{ 114 | Namespace: "tacquito", 115 | Name: "serve_connection_duration_milliseconds", 116 | Help: "total time time of a net.Conn, including overhead, in milliseconds", 117 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, 118 | }, 119 | ) 120 | ) 121 | 122 | func init() { 123 | // gauges and counters 124 | prometheus.MustRegister(serveReceived) 125 | prometheus.MustRegister(serveAccepted) 126 | prometheus.MustRegister(serveAcceptedError) 127 | prometheus.MustRegister(handlers) 128 | prometheus.MustRegister(crypterRead) 129 | prometheus.MustRegister(crypterReadError) 130 | prometheus.MustRegister(crypterWrite) 131 | prometheus.MustRegister(crypterWriteError) 132 | prometheus.MustRegister(crypterBadSecret) 133 | prometheus.MustRegister(crypterUnmarshalError) 134 | prometheus.MustRegister(crypterMarshalError) 135 | prometheus.MustRegister(crypterCryptError) 136 | prometheus.MustRegister(waitgroupActive) 137 | prometheus.MustRegister(sessionsActive) 138 | prometheus.MustRegister(sessionsGetHit) 139 | prometheus.MustRegister(sessionsGetMiss) 140 | prometheus.MustRegister(sessionsSet) 141 | // durations 142 | prometheus.MustRegister(sessionDurations) 143 | prometheus.MustRegister(connectionDuration) 144 | } 145 | --------------------------------------------------------------------------------